@delt/claude-jev-advisor 0.1.1 → 0.2.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.
@@ -4,21 +4,24 @@
4
4
  import os from "os";
5
5
 
6
6
  // src/display/statusline-run.ts
7
- import { spawnSync } from "child_process";
7
+ import { spawn, spawnSync } from "child_process";
8
8
  import fs3 from "fs";
9
+ import path4 from "path";
9
10
 
10
11
  // src/config.ts
11
12
  import fs from "fs";
12
13
  import path2 from "path";
13
14
 
14
15
  // src/config-shape.ts
16
+ var CONFIG_VERSION = 2;
15
17
  var DEFAULT_CONFIG = {
16
18
  lang: "ko",
17
19
  keyFile: null,
18
20
  display: "mod",
19
- context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
21
+ context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
20
22
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
21
23
  };
24
+ var LEGACY_CONTEXT_DEFAULTS = { minTokens: 1e5, compactMinTokens: 2e5, unitDoneYes: 0.7 };
22
25
  var LANGS = ["ko", "en"];
23
26
  var DISPLAYS = ["mod", "statusline", "message"];
24
27
  function isRecord(value) {
@@ -32,13 +35,17 @@ function mergeSection(defaults, raw) {
32
35
  }
33
36
  return out;
34
37
  }
38
+ function withoutLegacyDefaults(context) {
39
+ if (!isRecord(context)) return context;
40
+ return Object.fromEntries(Object.entries(context).filter(([key, value]) => LEGACY_CONTEXT_DEFAULTS[key] !== value));
41
+ }
35
42
  function normalizeConfig(raw) {
36
43
  const r = isRecord(raw) ? raw : {};
37
44
  return {
38
45
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
39
46
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
40
47
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
41
- context: mergeSection(DEFAULT_CONFIG.context, r.context),
48
+ context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
42
49
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
43
50
  };
44
51
  }
@@ -87,6 +94,11 @@ function formatSize(tokens) {
87
94
  function remainingPct(size, threshold) {
88
95
  return Math.max(0, Math.round((threshold - size) / threshold * 100));
89
96
  }
97
+ function adviceKind(size, judgment, config) {
98
+ if (size === null || size < config.context.minTokens || !judgment || judgment.phase === "working") return null;
99
+ if (judgment.clear) return "clear";
100
+ return size >= config.context.compactMinTokens ? "compact" : null;
101
+ }
90
102
  function adviceLine({ size, threshold, judgment, config }) {
91
103
  if (size === null) return "";
92
104
  const k = formatSize(size);
@@ -99,10 +111,8 @@ function adviceLine({ size, threshold, judgment, config }) {
99
111
  return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
100
112
  }
101
113
  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}`;
114
+ const kind = adviceKind(size, counted, config);
115
+ return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
106
116
  }
107
117
  function usableJudgment(state, liveSize, turnStartedAt) {
108
118
  if (!state || !state.judgment || state.at < turnStartedAt) return null;
@@ -133,9 +143,39 @@ function readState(home, sessionId) {
133
143
 
134
144
  // src/display/statusline-run.ts
135
145
  var BEFORE_TIMEOUT_MS = 2e3;
146
+ var DEFAULT_GIT_BASH = "C:\\Program Files\\Git\\bin\\bash.exe";
136
147
  function isRecord3(value) {
137
148
  return typeof value === "object" && value !== null && !Array.isArray(value);
138
149
  }
150
+ function findGitBash(env, exists) {
151
+ const given = env.CLAUDE_CODE_GIT_BASH_PATH;
152
+ if (given && exists(given)) return given;
153
+ const candidates = [];
154
+ for (const dir of (env.PATH ?? env.Path ?? "").split(";").filter(Boolean)) {
155
+ if (!exists(path4.win32.join(dir, "git.exe"))) continue;
156
+ const parent = path4.win32.dirname(dir);
157
+ const root = path4.win32.basename(parent).toLowerCase() === "mingw64" ? path4.win32.dirname(parent) : parent;
158
+ candidates.push(path4.win32.join(root, "bin", "bash.exe"));
159
+ }
160
+ candidates.push(DEFAULT_GIT_BASH);
161
+ return candidates.find(exists) ?? null;
162
+ }
163
+ function statusLineShell(platform, env, exists) {
164
+ if (platform !== "win32") return { file: "/bin/sh", args: ["-c"] };
165
+ const bash = findGitBash(env, exists);
166
+ return bash ? { file: bash, args: ["-c"] } : { file: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-Command"] };
167
+ }
168
+ function killTree(pid) {
169
+ if (pid === void 0) return;
170
+ if (process.platform === "win32") {
171
+ spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
172
+ return;
173
+ }
174
+ try {
175
+ process.kill(-pid, "SIGKILL");
176
+ } catch {
177
+ }
178
+ }
139
179
  function previousStatusLine(home) {
140
180
  try {
141
181
  const value = JSON.parse(fs3.readFileSync(statusLineBeforePath(home), "utf8"));
@@ -144,9 +184,39 @@ function previousStatusLine(home) {
144
184
  return null;
145
185
  }
146
186
  }
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 : "";
187
+ function runBeforeCommand(command, input, shell = statusLineShell(process.platform, process.env, fs3.existsSync), timeoutMs = BEFORE_TIMEOUT_MS) {
188
+ return new Promise((resolve) => {
189
+ let child;
190
+ try {
191
+ child = spawn(shell.file, [...shell.args, command], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true, detached: process.platform !== "win32" });
192
+ } catch {
193
+ resolve("");
194
+ return;
195
+ }
196
+ let out = "";
197
+ let settled = false;
198
+ const settle = (text) => {
199
+ if (settled) return;
200
+ settled = true;
201
+ clearTimeout(timer);
202
+ resolve(text);
203
+ };
204
+ const timer = setTimeout(() => {
205
+ killTree(child.pid);
206
+ child.stdout?.destroy();
207
+ child.unref();
208
+ settle("");
209
+ }, timeoutMs);
210
+ child.stdout?.setEncoding("utf8");
211
+ child.stdout?.on("data", (chunk) => {
212
+ out += chunk;
213
+ });
214
+ child.on("error", () => settle(""));
215
+ child.on("close", (code) => settle(code === 0 ? out : ""));
216
+ child.stdin?.on("error", () => {
217
+ });
218
+ child.stdin?.end(input);
219
+ });
150
220
  }
151
221
  function ourLine(raw, home) {
152
222
  let input;
@@ -162,10 +232,10 @@ function ourLine(raw, home) {
162
232
  const judgment = usableJudgment(readState(home, input.session_id), size, 0);
163
233
  return adviceLine({ size, threshold: null, judgment, config });
164
234
  }
165
- function runStatusLine(raw, deps) {
235
+ async function runStatusLine(raw, deps) {
166
236
  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(" ");
237
+ const userLine = before ? (await deps.runBefore(before.command, raw)).replace(/\s+$/, "") : "";
238
+ return [userLine, ourLine(raw, deps.home)].filter(Boolean).join(" ");
169
239
  }
170
240
 
171
241
  // src/display/statusline.ts
@@ -173,7 +243,7 @@ async function main() {
173
243
  let raw = "";
174
244
  process.stdin.setEncoding("utf8");
175
245
  for await (const chunk of process.stdin) raw += chunk;
176
- const out = runStatusLine(raw, { home: os.homedir(), runBefore: runBeforeCommand });
246
+ const out = await runStatusLine(raw, { home: os.homedir(), runBefore: (command, input) => runBeforeCommand(command, input) });
177
247
  if (out) process.stdout.write(out);
178
248
  }
179
249
  main().catch(() => {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jev-advisor",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Bottom-row compact/clear advice from @delt/claude-jev-advisor (unofficial)",
5
5
  "author": { "name": "delt96" },
6
6
  "userConfig": {
@@ -1,11 +1,13 @@
1
1
  // src/config-shape.ts
2
+ var CONFIG_VERSION = 2;
2
3
  var DEFAULT_CONFIG = {
3
4
  lang: "ko",
4
5
  keyFile: null,
5
6
  display: "mod",
6
- context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
7
+ context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
7
8
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
8
9
  };
10
+ var LEGACY_CONTEXT_DEFAULTS = { minTokens: 1e5, compactMinTokens: 2e5, unitDoneYes: 0.7 };
9
11
  var LANGS = ["ko", "en"];
10
12
  var DISPLAYS = ["mod", "statusline", "message"];
11
13
  function isRecord(value) {
@@ -19,13 +21,17 @@ function mergeSection(defaults, raw) {
19
21
  }
20
22
  return out;
21
23
  }
24
+ function withoutLegacyDefaults(context) {
25
+ if (!isRecord(context)) return context;
26
+ return Object.fromEntries(Object.entries(context).filter(([key, value]) => LEGACY_CONTEXT_DEFAULTS[key] !== value));
27
+ }
22
28
  function normalizeConfig(raw) {
23
29
  const r = isRecord(raw) ? raw : {};
24
30
  return {
25
31
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
26
32
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
27
33
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
28
- context: mergeSection(DEFAULT_CONFIG.context, r.context),
34
+ context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
29
35
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
30
36
  };
31
37
  }
@@ -53,6 +59,11 @@ function formatSize(tokens) {
53
59
  function remainingPct(size, threshold) {
54
60
  return Math.max(0, Math.round((threshold - size) / threshold * 100));
55
61
  }
62
+ function adviceKind(size, judgment, config) {
63
+ if (size === null || size < config.context.minTokens || !judgment || judgment.phase === "working") return null;
64
+ if (judgment.clear) return "clear";
65
+ return size >= config.context.compactMinTokens ? "compact" : null;
66
+ }
56
67
  function adviceLine({ size, threshold, judgment, config }) {
57
68
  if (size === null) return "";
58
69
  const k = formatSize(size);
@@ -65,10 +76,8 @@ function adviceLine({ size, threshold, judgment, config }) {
65
76
  return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
66
77
  }
67
78
  if (!counted) return k;
68
- if (counted.phase === "working") return `\u{1F7E2} ${k}`;
69
- if (counted.clear) return `\u{1F7E1} ${k} ${phrases.clear}`;
70
- if (size >= config.context.compactMinTokens) return `\u{1F7E1} ${k} ${phrases.compact}`;
71
- return `\u{1F7E2} ${k}`;
79
+ const kind = adviceKind(size, counted, config);
80
+ return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
72
81
  }
73
82
  function usableJudgment(state, liveSize2, turnStartedAt) {
74
83
  if (!state || !state.judgment || state.at < turnStartedAt) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delt/claude-jev-advisor",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Unofficial Claude Code helpers using TypeSafe Jev: compact/clear advice and an rm guard",
5
5
  "type": "module",
6
6
  "bin": {