@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.
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/rm/hook.ts
4
+ import os from "os";
5
+
6
+ // src/rm/guard.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { spawnSync } from "child_process";
10
+ var win = path.win32;
11
+ var SEPARATORS = /* @__PURE__ */ new Set([";", "&&", "||", "|", "|&", "&", "(", ")"]);
12
+ var SKIP_WORDS = /* @__PURE__ */ new Set(["then", "else", "elif", "do", "while", "until", "if", "!", "{", "time"]);
13
+ var HEADER_WORDS = /* @__PURE__ */ new Set(["for", "select", "case"]);
14
+ var CLOSING_WORDS = /* @__PURE__ */ new Set(["done", "fi", "esac", "}"]);
15
+ var BUILD_DIRS = /* @__PURE__ */ new Set(["target", "build", "dist", "out", "node_modules", "coverage", "__pycache__", ".pytest_cache", ".gradle", ".next", ".nuxt", ".turbo", ".cache", "bin", "obj"]);
16
+ var MAX_LISTED = 3;
17
+ var ASSIGNMENT = /^([A-Za-z_][A-Za-z0-9_]*)=/;
18
+ var DECLARERS = /* @__PURE__ */ new Set(["export", "readonly", "declare", "typeset", "local"]);
19
+ var KNOWN_ENV = ["TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE"];
20
+ function stripHeredocs(text) {
21
+ const out = [];
22
+ const pending = [];
23
+ for (const line of text.split("\n")) {
24
+ if (pending.length) {
25
+ const { dash, word } = pending[0];
26
+ if ((dash ? line.replace(/^\t+/, "") : line).trimEnd() === word) pending.shift();
27
+ continue;
28
+ }
29
+ out.push(line);
30
+ for (const m of line.matchAll(/(?<!<)<<(?!<)(-?)\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2/g)) pending.push({ dash: m[1] === "-", word: m[3] });
31
+ }
32
+ return out.join("\n");
33
+ }
34
+ function tokenize(text) {
35
+ const tokens = [];
36
+ let i = 0;
37
+ let word = null;
38
+ let skipNextWord = false;
39
+ const start = () => word ??= { value: "", dynamic: false, glob: false, tilde: false, brace: false };
40
+ const finish = () => {
41
+ if (word === null) return;
42
+ if (skipNextWord) skipNextWord = false;
43
+ else tokens.push({ type: "word", ...word });
44
+ word = null;
45
+ };
46
+ const readBalanced = (open, close) => {
47
+ let depth = 0;
48
+ const from = i;
49
+ for (; i < text.length; i++) {
50
+ if (text[i] === open) depth++;
51
+ else if (text[i] === close && --depth === 0) {
52
+ i++;
53
+ return text.slice(from, i);
54
+ }
55
+ }
56
+ return text.slice(from);
57
+ };
58
+ const readDollar = () => {
59
+ const w = start();
60
+ w.dynamic = true;
61
+ if (text[i + 1] === "(") {
62
+ w.value += "$";
63
+ i++;
64
+ w.value += readBalanced("(", ")");
65
+ return;
66
+ }
67
+ if (text[i + 1] === "{") {
68
+ w.value += "$";
69
+ i++;
70
+ w.value += readBalanced("{", "}");
71
+ return;
72
+ }
73
+ const m = /^\$([A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-])/.exec(text.slice(i));
74
+ if (m) {
75
+ w.value += m[0];
76
+ i += m[0].length;
77
+ return;
78
+ }
79
+ w.dynamic = false;
80
+ w.value += "$";
81
+ i++;
82
+ };
83
+ while (i < text.length) {
84
+ const c = text[i];
85
+ if (c === " " || c === " " || c === "\r") {
86
+ finish();
87
+ i++;
88
+ continue;
89
+ }
90
+ if (c === "\n") {
91
+ finish();
92
+ tokens.push({ type: "op", value: ";" });
93
+ i++;
94
+ continue;
95
+ }
96
+ if (c === "#" && word === null) {
97
+ while (i < text.length && text[i] !== "\n") i++;
98
+ continue;
99
+ }
100
+ if (c === "'") {
101
+ const end = text.indexOf("'", i + 1);
102
+ start().value += text.slice(i + 1, end === -1 ? text.length : end);
103
+ i = end === -1 ? text.length : end + 1;
104
+ continue;
105
+ }
106
+ if (c === '"') {
107
+ const w2 = start();
108
+ i++;
109
+ while (i < text.length && text[i] !== '"') {
110
+ if (text[i] === "\\" && '"\\$`'.includes(text[i + 1] ?? "")) {
111
+ w2.value += text[i + 1];
112
+ i += 2;
113
+ continue;
114
+ }
115
+ if (text[i] === "$") {
116
+ readDollar();
117
+ continue;
118
+ }
119
+ if (text[i] === "`") {
120
+ w2.dynamic = true;
121
+ const end = text.indexOf("`", i + 1);
122
+ w2.value += text.slice(i, end === -1 ? text.length : end + 1);
123
+ i = end === -1 ? text.length : end + 1;
124
+ continue;
125
+ }
126
+ w2.value += text[i++];
127
+ }
128
+ i++;
129
+ continue;
130
+ }
131
+ if (c === "\\") {
132
+ start().value += text[i + 1] ?? "";
133
+ i += 2;
134
+ continue;
135
+ }
136
+ if (c === "$") {
137
+ readDollar();
138
+ continue;
139
+ }
140
+ if (c === "`") {
141
+ const w2 = start();
142
+ w2.dynamic = true;
143
+ const end = text.indexOf("`", i + 1);
144
+ w2.value += text.slice(i, end === -1 ? text.length : end + 1);
145
+ i = end === -1 ? text.length : end + 1;
146
+ continue;
147
+ }
148
+ if (c === ">" || c === "<" || c === "&" && text[i + 1] === ">") {
149
+ if (word !== null && /^\d+$/.test(word.value) && !word.dynamic) word = null;
150
+ else finish();
151
+ const m = /^(&>>?|\d*[<>]+[&|]?)/.exec(text.slice(i));
152
+ i += m[0].length;
153
+ if (/&$/.test(m[0]) && /^\d|^-/.test(text[i] ?? "")) {
154
+ while (i < text.length && /[\d-]/.test(text[i])) i++;
155
+ continue;
156
+ }
157
+ skipNextWord = true;
158
+ continue;
159
+ }
160
+ const op = ["&&", "||", "|&", ";;", ";", "|", "&", "(", ")"].find((o) => text.startsWith(o, i));
161
+ if (op) {
162
+ finish();
163
+ tokens.push({ type: "op", value: op === ";;" ? ";" : op });
164
+ i += op.length;
165
+ continue;
166
+ }
167
+ const w = start();
168
+ if (c === "~" && w.value === "") w.tilde = true;
169
+ if ("*?[".includes(c)) w.glob = true;
170
+ if (c === "{" && /^\{[^}]*,[^}]*\}/.test(text.slice(i))) w.brace = true;
171
+ w.value += c;
172
+ i++;
173
+ }
174
+ finish();
175
+ return tokens;
176
+ }
177
+ function splitCommands(tokens) {
178
+ const commands = [];
179
+ let current = [];
180
+ for (const t of tokens) {
181
+ if (t.type === "op" && SEPARATORS.has(t.value)) {
182
+ commands.push({ words: current, after: t.value });
183
+ current = [];
184
+ } else if (t.type === "word") current.push(t);
185
+ }
186
+ commands.push({ words: current, after: null });
187
+ return commands;
188
+ }
189
+ function expand(word, vars) {
190
+ if (!word.dynamic) return word.value;
191
+ let known = true;
192
+ const out = word.value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (m, braced, bare) => {
193
+ const v = vars.get(braced ?? bare ?? "");
194
+ if (typeof v !== "string") {
195
+ known = false;
196
+ return m;
197
+ }
198
+ return v;
199
+ });
200
+ return known && !/[$`]/.test(out) ? out : null;
201
+ }
202
+ function assign(word, vars, home) {
203
+ const name = ASSIGNMENT.exec(word.value)[1];
204
+ let value = expand({ value: word.value.slice(name.length + 1), dynamic: word.dynamic }, vars);
205
+ if (value !== null && /^~(?:[\\/]|$)/.test(value)) value = home + value.slice(1);
206
+ vars.set(name, value);
207
+ }
208
+ function commandName(word) {
209
+ if (word.dynamic) return null;
210
+ return win.basename(word.value.replace(/\//g, "\\")).replace(/\.exe$/i, "").toLowerCase();
211
+ }
212
+ function stripPrefixes(words) {
213
+ let rest = words;
214
+ for (; ; ) {
215
+ while (rest.length && ASSIGNMENT.test(rest[0].value)) rest = rest.slice(1);
216
+ if (!rest.length) return rest;
217
+ const name = commandName(rest[0]);
218
+ if (SKIP_WORDS.has(rest[0].value) || name === "command" || name === "builtin" || name === "exec" || name === "nohup") {
219
+ rest = rest.slice(1);
220
+ continue;
221
+ }
222
+ if (name === "sudo" || name === "env") {
223
+ rest = rest.slice(1);
224
+ while (rest.length && !rest[0].dynamic && rest[0].value.startsWith("-")) rest = rest.slice(1);
225
+ continue;
226
+ }
227
+ if (name === "timeout") {
228
+ rest = rest.slice(1);
229
+ while (rest.length && !rest[0].dynamic && rest[0].value.startsWith("-")) rest = rest.slice(1);
230
+ rest = rest.slice(1);
231
+ continue;
232
+ }
233
+ return rest;
234
+ }
235
+ }
236
+ function toWindowsPath(word, cwd, home, tmpdir) {
237
+ const v = word.value;
238
+ if (word.tilde && (v === "~" || v.startsWith("~/") || v.startsWith("~\\"))) return { path: win.resolve(home, v.slice(2) || ".") };
239
+ if (/^[A-Za-z]:[\\/]/.test(v)) return { path: win.normalize(v) };
240
+ const drive = /^\/([A-Za-z])(?:\/(.*))?$/.exec(v);
241
+ if (drive) return { path: win.normalize(`${drive[1].toUpperCase()}:\\${drive[2] ?? ""}`) };
242
+ const tmp = /^\/tmp(?:\/(.*))?$/.exec(v);
243
+ if (tmp) return { path: win.resolve(tmpdir, tmp[1] ?? ".") };
244
+ if (v.startsWith("/")) return { unknown: v };
245
+ if (cwd === null) return { unresolvable: "a relative path after cd to a path that cannot be worked out" };
246
+ return { path: win.resolve(cwd, v) };
247
+ }
248
+ function isInside(child, parent) {
249
+ const c = child.toLowerCase();
250
+ const p = parent.toLowerCase().replace(/\\+$/, "");
251
+ return c.startsWith(`${p}\\`);
252
+ }
253
+ function segments(p) {
254
+ return p.toLowerCase().split("\\");
255
+ }
256
+ function classify(target, ctx) {
257
+ if (target.brace) return { unresolvable: `brace expansion (${target.value})` };
258
+ const value = expand(target, ctx.vars);
259
+ if (value === null) return { unresolvable: `a shell variable or command substitution that cannot be worked out (${target.value})` };
260
+ let w = { ...target, value, dynamic: false };
261
+ let pattern = null;
262
+ if (target.glob) {
263
+ const parts = value.split(/[\\/]/);
264
+ if (parts.slice(0, -1).some((s) => /[*?[]/.test(s))) return { unresolvable: `a wildcard in a folder name (${target.value})` };
265
+ pattern = parts[parts.length - 1];
266
+ w = { ...w, value: parts.slice(0, -1).join("/") || "." };
267
+ }
268
+ const resolved = toWindowsPath(w, ctx.cwd, ctx.home, ctx.tmpdirs[0]);
269
+ if (resolved.unresolvable) return { unresolvable: resolved.unresolvable };
270
+ if (resolved.unknown) return { real: target.value };
271
+ const p = resolved.path;
272
+ const shown = pattern === null ? p : `${p}\\${pattern}`;
273
+ const narrowPattern = pattern !== null && !/^[*?.]+$/.test(pattern);
274
+ if (ctx.tmpdirs.some((t) => isInside(p, t) || narrowPattern && p.toLowerCase() === t.toLowerCase().replace(/\\+$/, ""))) return {};
275
+ if (segments(p).includes(".superpowers")) return {};
276
+ if (pattern === null && !ctx.probe.exists(p)) return {};
277
+ if (segments(p).some((s) => BUILD_DIRS.has(s)) && ctx.probe.isIgnored(p)) return {};
278
+ return { real: shown };
279
+ }
280
+ function deny(why) {
281
+ return {
282
+ decision: "deny",
283
+ reason: `rm-guard could not work out what this rm would delete (${why}). Rewrite it with literal paths: no shell variables, command substitutions, xargs, brace expansion or wildcards in folder names, and no cd to such a path before it. Example: rm -f "C:/full/path/file.txt". If you need a variable or a listing to find the paths, run that on its own first, then rm the printed paths.`
284
+ };
285
+ }
286
+ function decide({ command, cwd, home, tmpdirs, env = {}, probe }) {
287
+ if (!/\b(?:rm|rmdir|xargs)\b/.test(command)) return null;
288
+ const commands = splitCommands(tokenize(stripHeredocs(command)));
289
+ const vars = new Map(KNOWN_ENV.filter((n) => env[n]).map((n) => [n, env[n]]));
290
+ const ctx = { cwd, home, tmpdirs, probe, vars };
291
+ const setCwd = (next) => {
292
+ ctx.cwd = next;
293
+ vars.set("PWD", next);
294
+ };
295
+ setCwd(cwd);
296
+ const stack = [];
297
+ const real = [];
298
+ for (const { words, after } of commands) {
299
+ const rest = stripPrefixes(words);
300
+ if (!rest.length) {
301
+ for (const w of words) if (ASSIGNMENT.test(w.value)) assign(w, vars, home);
302
+ } else if (HEADER_WORDS.has(rest[0].value)) {
303
+ if (rest[0].value !== "case" && rest[1]) vars.set(rest[1].value, null);
304
+ } else if (!CLOSING_WORDS.has(rest[0].value)) {
305
+ const name = commandName(rest[0]);
306
+ if (name !== null && DECLARERS.has(name)) {
307
+ for (const w of rest.slice(1)) if (ASSIGNMENT.test(w.value)) assign(w, vars, home);
308
+ } else if (name === "cd" || name === "pushd" || name === "popd") {
309
+ const arg = rest[1];
310
+ const value = arg ? expand(arg, vars) : null;
311
+ if (name === "popd" || arg && (value === null || value === "-")) setCwd(null);
312
+ else if (!arg) setCwd(home);
313
+ else setCwd(toWindowsPath({ ...arg, value }, ctx.cwd, home, tmpdirs[0]).path ?? null);
314
+ } else if (name === "xargs") {
315
+ if (rest.slice(1).some((t) => {
316
+ const n = commandName(t);
317
+ return n === "rm" || n === "rmdir";
318
+ })) {
319
+ return deny("xargs feeding rm");
320
+ }
321
+ } else if (name === "rm" || name === "rmdir") {
322
+ let options = true;
323
+ for (const t of rest.slice(1)) {
324
+ if (options && !t.dynamic && t.value === "--") {
325
+ options = false;
326
+ continue;
327
+ }
328
+ if (options && !t.dynamic && t.value.length > 1 && t.value.startsWith("-")) continue;
329
+ const c = classify(t, ctx);
330
+ if (c.unresolvable) return deny(c.unresolvable);
331
+ if (c.real) real.push(c.real);
332
+ }
333
+ }
334
+ }
335
+ if (after === "(") stack.push(ctx.cwd);
336
+ if (after === ")" && stack.length) setCwd(stack.pop() ?? null);
337
+ }
338
+ if (!real.length) return null;
339
+ const listed = real.slice(0, MAX_LISTED).join(", ");
340
+ const more = real.length > MAX_LISTED ? ` \uC678 ${real.length - MAX_LISTED}\uAC1C` : "";
341
+ return { decision: "ask", reason: `\uC2E4\uC81C \uD30C\uC77C \uC0AD\uC81C: ${listed}${more}` };
342
+ }
343
+ function hookOutput({ decision, reason }) {
344
+ return JSON.stringify({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: decision, permissionDecisionReason: reason } });
345
+ }
346
+ var realProbe = {
347
+ exists(p) {
348
+ try {
349
+ fs.lstatSync(p);
350
+ return true;
351
+ } catch {
352
+ return false;
353
+ }
354
+ },
355
+ isIgnored(p) {
356
+ const r = spawnSync("git", ["-C", win.dirname(p), "check-ignore", "-q", "--", p], { timeout: 3e3, windowsHide: true });
357
+ return r.status === 0;
358
+ }
359
+ };
360
+
361
+ // src/rm/run.ts
362
+ import path4 from "path";
363
+
364
+ // src/config.ts
365
+ import fs2 from "fs";
366
+ import path3 from "path";
367
+
368
+ // src/config-shape.ts
369
+ var DEFAULT_CONFIG = {
370
+ lang: "ko",
371
+ keyFile: null,
372
+ display: "mod",
373
+ context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
374
+ rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
375
+ };
376
+ var LANGS = ["ko", "en"];
377
+ var DISPLAYS = ["mod", "statusline", "message"];
378
+ function isRecord(value) {
379
+ return typeof value === "object" && value !== null && !Array.isArray(value);
380
+ }
381
+ function mergeSection(defaults, raw) {
382
+ const out = { ...defaults };
383
+ if (!isRecord(raw)) return out;
384
+ for (const [key, value] of Object.entries(defaults)) {
385
+ if (typeof raw[key] === typeof value) out[key] = raw[key];
386
+ }
387
+ return out;
388
+ }
389
+ function normalizeConfig(raw) {
390
+ const r = isRecord(raw) ? raw : {};
391
+ return {
392
+ lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
393
+ keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
394
+ display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
395
+ context: mergeSection(DEFAULT_CONFIG.context, r.context),
396
+ rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
397
+ };
398
+ }
399
+
400
+ // src/paths.ts
401
+ import path2 from "path";
402
+ var claudeDir = (home) => path2.join(home, ".claude");
403
+ var dataDir = (home) => path2.join(claudeDir(home), "claude-jev-advisor");
404
+ var configPath = (home) => path2.join(dataDir(home), "config.json");
405
+
406
+ // src/config.ts
407
+ function readConfig(home) {
408
+ try {
409
+ return normalizeConfig(JSON.parse(fs2.readFileSync(configPath(home), "utf8").replace(/^\uFEFF/, "")));
410
+ } catch {
411
+ return normalizeConfig({});
412
+ }
413
+ }
414
+
415
+ // src/rm/run.ts
416
+ function runRmHook(raw, deps) {
417
+ if (!readConfig(deps.home).rm.enabled) return null;
418
+ const input = JSON.parse(raw);
419
+ if (typeof input !== "object" || input === null) return null;
420
+ const command = input.tool_input?.command;
421
+ if (input.tool_name !== "Bash" || typeof command !== "string") return null;
422
+ const tmpdirs = [...new Set([deps.tmpdir, deps.env.TEMP, deps.env.TMP].filter((t) => Boolean(t)).map((t) => path4.win32.resolve(t)))];
423
+ const cwd = typeof input.cwd === "string" && input.cwd ? input.cwd : deps.cwd;
424
+ const result = decide({ command, cwd, home: deps.home, tmpdirs, env: deps.env, probe: deps.probe });
425
+ return result ? hookOutput(result) : null;
426
+ }
427
+
428
+ // src/rm/hook.ts
429
+ async function main() {
430
+ let raw = "";
431
+ process.stdin.setEncoding("utf8");
432
+ for await (const chunk of process.stdin) raw += chunk;
433
+ const out = runRmHook(raw, { home: os.homedir(), env: process.env, tmpdir: os.tmpdir(), cwd: process.cwd(), probe: realProbe });
434
+ if (out) process.stdout.write(out);
435
+ }
436
+ main().catch(() => {
437
+ });
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/display/statusline.ts
4
+ import os from "os";
5
+
6
+ // src/display/statusline-run.ts
7
+ import { spawnSync } from "child_process";
8
+ import fs3 from "fs";
9
+
10
+ // src/config.ts
11
+ import fs from "fs";
12
+ import path2 from "path";
13
+
14
+ // src/config-shape.ts
15
+ var DEFAULT_CONFIG = {
16
+ lang: "ko",
17
+ keyFile: null,
18
+ display: "mod",
19
+ context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
20
+ rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
21
+ };
22
+ var LANGS = ["ko", "en"];
23
+ var DISPLAYS = ["mod", "statusline", "message"];
24
+ function isRecord(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function mergeSection(defaults, raw) {
28
+ const out = { ...defaults };
29
+ if (!isRecord(raw)) return out;
30
+ for (const [key, value] of Object.entries(defaults)) {
31
+ if (typeof raw[key] === typeof value) out[key] = raw[key];
32
+ }
33
+ return out;
34
+ }
35
+ function normalizeConfig(raw) {
36
+ const r = isRecord(raw) ? raw : {};
37
+ return {
38
+ lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
39
+ keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
40
+ display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
41
+ context: mergeSection(DEFAULT_CONFIG.context, r.context),
42
+ rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
43
+ };
44
+ }
45
+
46
+ // src/paths.ts
47
+ import path from "path";
48
+ var claudeDir = (home) => path.join(home, ".claude");
49
+ var dataDir = (home) => path.join(claudeDir(home), "claude-jev-advisor");
50
+ var configPath = (home) => path.join(dataDir(home), "config.json");
51
+ var stateDir = (home) => path.join(dataDir(home), "state");
52
+ var statusLineBeforePath = (home) => path.join(dataDir(home), "statusline-before.json");
53
+
54
+ // src/config.ts
55
+ function readConfig(home) {
56
+ try {
57
+ return normalizeConfig(JSON.parse(fs.readFileSync(configPath(home), "utf8").replace(/^\uFEFF/, "")));
58
+ } catch {
59
+ return normalizeConfig({});
60
+ }
61
+ }
62
+
63
+ // src/context/files.ts
64
+ import fs2 from "fs";
65
+ import path3 from "path";
66
+
67
+ // src/display/line.ts
68
+ var STALE_SIZE_RATIO = 0.7;
69
+ var PHRASES = {
70
+ ko: {
71
+ clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
72
+ compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
73
+ compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
74
+ },
75
+ en: {
76
+ clear: "Start fresh? /clear",
77
+ compact: "Wrap up what you have and continue? /compact",
78
+ compactLater: "When this work is done, wrap up and continue? /compact"
79
+ }
80
+ };
81
+ function isRecord2(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+ function formatSize(tokens) {
85
+ return `${Math.round(tokens / 1e3)}k`;
86
+ }
87
+ function remainingPct(size, threshold) {
88
+ return Math.max(0, Math.round((threshold - size) / threshold * 100));
89
+ }
90
+ function adviceLine({ size, threshold, judgment, config }) {
91
+ if (size === null) return "";
92
+ const k = formatSize(size);
93
+ const phrases = PHRASES[config.lang];
94
+ const counted = size >= config.context.minTokens ? judgment : null;
95
+ if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
96
+ const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
97
+ if (!counted) return head;
98
+ if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
99
+ return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
100
+ }
101
+ if (!counted) return k;
102
+ if (counted.phase === "working") return `\u{1F7E2} ${k}`;
103
+ if (counted.clear) return `\u{1F7E1} ${k} ${phrases.clear}`;
104
+ if (size >= config.context.compactMinTokens) return `\u{1F7E1} ${k} ${phrases.compact}`;
105
+ return `\u{1F7E2} ${k}`;
106
+ }
107
+ function usableJudgment(state, liveSize, turnStartedAt) {
108
+ if (!state || !state.judgment || state.at < turnStartedAt) return null;
109
+ if (liveSize !== null && state.size !== null && liveSize < state.size * STALE_SIZE_RATIO) return null;
110
+ return state.judgment;
111
+ }
112
+ function parseState(raw) {
113
+ if (!isRecord2(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
114
+ const j = raw.judgment;
115
+ const judgment = isRecord2(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
116
+ return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
117
+ }
118
+
119
+ // src/context/files.ts
120
+ var SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
121
+ function statePath(home, sessionId) {
122
+ return SAFE_SESSION_ID.test(sessionId) ? path3.join(stateDir(home), `${sessionId}.json`) : null;
123
+ }
124
+ function readState(home, sessionId) {
125
+ const file = statePath(home, sessionId);
126
+ if (!file) return null;
127
+ try {
128
+ return parseState(JSON.parse(fs2.readFileSync(file, "utf8")));
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ // src/display/statusline-run.ts
135
+ var BEFORE_TIMEOUT_MS = 2e3;
136
+ function isRecord3(value) {
137
+ return typeof value === "object" && value !== null && !Array.isArray(value);
138
+ }
139
+ function previousStatusLine(home) {
140
+ try {
141
+ const value = JSON.parse(fs3.readFileSync(statusLineBeforePath(home), "utf8"));
142
+ return isRecord3(value) && typeof value.command === "string" ? { command: value.command } : null;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+ function runBeforeCommand(command, input) {
148
+ const r = spawnSync(command, { shell: true, input, encoding: "utf8", timeout: BEFORE_TIMEOUT_MS, windowsHide: true });
149
+ return r.status === 0 && typeof r.stdout === "string" ? r.stdout : "";
150
+ }
151
+ function ourLine(raw, home) {
152
+ let input;
153
+ try {
154
+ input = JSON.parse(raw);
155
+ } catch {
156
+ return "";
157
+ }
158
+ const config = readConfig(home);
159
+ if (!isRecord3(input) || typeof input.session_id !== "string" || !config.context.enabled || config.display !== "statusline") return "";
160
+ const window = isRecord3(input.context_window) ? input.context_window : {};
161
+ const size = typeof window.total_input_tokens === "number" && window.total_input_tokens > 0 ? window.total_input_tokens : null;
162
+ const judgment = usableJudgment(readState(home, input.session_id), size, 0);
163
+ return adviceLine({ size, threshold: null, judgment, config });
164
+ }
165
+ function runStatusLine(raw, deps) {
166
+ const before = previousStatusLine(deps.home);
167
+ const parts = [before ? deps.runBefore(before.command, raw).replace(/\s+$/, "") : "", ourLine(raw, deps.home)];
168
+ return parts.filter(Boolean).join(" ");
169
+ }
170
+
171
+ // src/display/statusline.ts
172
+ async function main() {
173
+ let raw = "";
174
+ process.stdin.setEncoding("utf8");
175
+ for await (const chunk of process.stdin) raw += chunk;
176
+ const out = runStatusLine(raw, { home: os.homedir(), runBefore: runBeforeCommand });
177
+ if (out) process.stdout.write(out);
178
+ }
179
+ main().catch(() => {
180
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "jev-advisor",
3
+ "version": "0.1.0",
4
+ "description": "Bottom-row compact/clear advice from @delt/claude-jev-advisor (unofficial)",
5
+ "author": { "name": "delt96" },
6
+ "userConfig": {
7
+ "dataDir": {
8
+ "type": "string",
9
+ "title": "Data folder",
10
+ "description": "The claude-jev-advisor data folder holding config.json and state/. Set by claude-jev-advisor install.",
11
+ "default": ""
12
+ }
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ { "modules": ["./register.js"] }