@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.
- package/README.md +52 -20
- package/dist/cli.js +248 -34
- package/dist/context-hook.js +111 -44
- package/dist/rm-hook.js +532 -25
- package/dist/statusline.js +84 -14
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +15 -6
- package/package.json +1 -1
package/dist/statusline.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
|
168
|
-
return
|
|
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(() => {
|
package/mod/hooks/register.js
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
69
|
-
|
|
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;
|