@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/LICENSE +21 -0
- package/README.md +153 -0
- package/dist/cli.js +642 -0
- package/dist/context-hook.js +394 -0
- package/dist/rm-hook.js +437 -0
- package/dist/statusline.js +180 -0
- package/mod/.claude-plugin/plugin.json +14 -0
- package/mod/hooks/hooks.json +1 -0
- package/mod/hooks/register.js +181 -0
- package/package.json +48 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// src/config-shape.ts
|
|
2
|
+
var DEFAULT_CONFIG = {
|
|
3
|
+
lang: "ko",
|
|
4
|
+
keyFile: null,
|
|
5
|
+
display: "mod",
|
|
6
|
+
context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
|
|
7
|
+
rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
|
|
8
|
+
};
|
|
9
|
+
var LANGS = ["ko", "en"];
|
|
10
|
+
var DISPLAYS = ["mod", "statusline", "message"];
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function mergeSection(defaults, raw) {
|
|
15
|
+
const out = { ...defaults };
|
|
16
|
+
if (!isRecord(raw)) return out;
|
|
17
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
18
|
+
if (typeof raw[key] === typeof value) out[key] = raw[key];
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
function normalizeConfig(raw) {
|
|
23
|
+
const r = isRecord(raw) ? raw : {};
|
|
24
|
+
return {
|
|
25
|
+
lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
|
|
26
|
+
keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
|
|
27
|
+
display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
|
|
28
|
+
context: mergeSection(DEFAULT_CONFIG.context, r.context),
|
|
29
|
+
rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/display/line.ts
|
|
34
|
+
var STALE_SIZE_RATIO = 0.7;
|
|
35
|
+
var PHRASES = {
|
|
36
|
+
ko: {
|
|
37
|
+
clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
|
|
38
|
+
compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
|
|
39
|
+
compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
|
|
40
|
+
},
|
|
41
|
+
en: {
|
|
42
|
+
clear: "Start fresh? /clear",
|
|
43
|
+
compact: "Wrap up what you have and continue? /compact",
|
|
44
|
+
compactLater: "When this work is done, wrap up and continue? /compact"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function isRecord2(value) {
|
|
48
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
function formatSize(tokens) {
|
|
51
|
+
return `${Math.round(tokens / 1e3)}k`;
|
|
52
|
+
}
|
|
53
|
+
function remainingPct(size, threshold) {
|
|
54
|
+
return Math.max(0, Math.round((threshold - size) / threshold * 100));
|
|
55
|
+
}
|
|
56
|
+
function adviceLine({ size, threshold, judgment, config }) {
|
|
57
|
+
if (size === null) return "";
|
|
58
|
+
const k = formatSize(size);
|
|
59
|
+
const phrases = PHRASES[config.lang];
|
|
60
|
+
const counted = size >= config.context.minTokens ? judgment : null;
|
|
61
|
+
if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
|
|
62
|
+
const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
|
|
63
|
+
if (!counted) return head;
|
|
64
|
+
if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
|
|
65
|
+
return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
|
|
66
|
+
}
|
|
67
|
+
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}`;
|
|
72
|
+
}
|
|
73
|
+
function usableJudgment(state, liveSize2, turnStartedAt) {
|
|
74
|
+
if (!state || !state.judgment || state.at < turnStartedAt) return null;
|
|
75
|
+
if (liveSize2 !== null && state.size !== null && liveSize2 < state.size * STALE_SIZE_RATIO) return null;
|
|
76
|
+
return state.judgment;
|
|
77
|
+
}
|
|
78
|
+
function parseState(raw) {
|
|
79
|
+
if (!isRecord2(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
|
|
80
|
+
const j = raw.judgment;
|
|
81
|
+
const judgment = isRecord2(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
|
|
82
|
+
return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/mod/register.ts
|
|
86
|
+
var REFRESH_MS = 2e3;
|
|
87
|
+
async function readJson($, file) {
|
|
88
|
+
try {
|
|
89
|
+
const text = await $.fs.read(file);
|
|
90
|
+
return typeof text === "string" ? JSON.parse(text.replace(/^\uFEFF/, "")) : void 0;
|
|
91
|
+
} catch {
|
|
92
|
+
return void 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function loadConfig($, dataDir) {
|
|
96
|
+
const raw = await readJson($, `${dataDir}/config.json`);
|
|
97
|
+
return raw === void 0 ? null : normalizeConfig(raw);
|
|
98
|
+
}
|
|
99
|
+
async function loadState($, dataDir) {
|
|
100
|
+
try {
|
|
101
|
+
const sessionId = await $.session.id();
|
|
102
|
+
const state = parseState(await readJson($, `${dataDir}/state/${sessionId}.json`));
|
|
103
|
+
return state && state.sessionId === sessionId ? state : null;
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function autoCompactThreshold($) {
|
|
109
|
+
try {
|
|
110
|
+
const usage = await $.session.usage({ breakdown: "summary" });
|
|
111
|
+
return usage.context.breakdown?.autoCompactThreshold ?? null;
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function liveSize($) {
|
|
117
|
+
try {
|
|
118
|
+
const tokens = (await $.session.usage()).context.tokens;
|
|
119
|
+
return typeof tokens === "number" && tokens > 0 ? tokens : null;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function register(on, options) {
|
|
125
|
+
const dataDir = typeof options.dataDir === "string" ? options.dataDir.replace(/[\\/]+$/, "") : "";
|
|
126
|
+
let config = null;
|
|
127
|
+
let state = null;
|
|
128
|
+
let threshold = null;
|
|
129
|
+
let turnStartedAt = 0;
|
|
130
|
+
let seen = "";
|
|
131
|
+
on("session.start", async ($, e, next) => {
|
|
132
|
+
const result = await next(e);
|
|
133
|
+
if (!dataDir || e.isInteractive === false) return result;
|
|
134
|
+
try {
|
|
135
|
+
config = await loadConfig($, dataDir);
|
|
136
|
+
threshold = await autoCompactThreshold($);
|
|
137
|
+
$.clock.every(REFRESH_MS, () => {
|
|
138
|
+
Promise.all([loadConfig($, dataDir), loadState($, dataDir), liveSize($)]).then(([nextConfig, nextState, size]) => {
|
|
139
|
+
const key = JSON.stringify([nextConfig, nextState, size === null ? null : Math.round(size / 1e3)]);
|
|
140
|
+
if (key === seen) return;
|
|
141
|
+
seen = key;
|
|
142
|
+
if (nextConfig) config = nextConfig;
|
|
143
|
+
state = nextState;
|
|
144
|
+
$.ui.invalidate("ui.render");
|
|
145
|
+
}).catch(() => {
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
});
|
|
152
|
+
on("turn.start", async ($, e, next) => {
|
|
153
|
+
try {
|
|
154
|
+
turnStartedAt = await $.clock.now();
|
|
155
|
+
$.ui.invalidate("ui.render");
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
return next(e);
|
|
159
|
+
});
|
|
160
|
+
on("turn.complete", async ($, e, next) => {
|
|
161
|
+
const result = await next(e);
|
|
162
|
+
threshold = await autoCompactThreshold($);
|
|
163
|
+
return result;
|
|
164
|
+
});
|
|
165
|
+
on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
|
|
166
|
+
let tail = "";
|
|
167
|
+
try {
|
|
168
|
+
const current = config;
|
|
169
|
+
if (current && current.context.enabled && current.display === "mod") {
|
|
170
|
+
const size = await liveSize($);
|
|
171
|
+
tail = adviceLine({ size, threshold, judgment: usableJudgment(state, size, turnStartedAt), config: current });
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
tail = "";
|
|
175
|
+
}
|
|
176
|
+
return tail ? next({ ...e, props: { ...e.props, tail } }) : next(e);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
register
|
|
181
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delt/claude-jev-advisor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Unofficial Claude Code helpers using TypeSafe Jev: compact/clear advice and an rm guard",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-jev-advisor": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"mod",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "node --import tsx --import ./test/isolate-home.ts --test --test-timeout=30000 \"test/**/*.test.ts\"",
|
|
21
|
+
"prepack": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"hooks",
|
|
27
|
+
"compact",
|
|
28
|
+
"context",
|
|
29
|
+
"typesafe",
|
|
30
|
+
"jev",
|
|
31
|
+
"rm"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/delt96/claude-jev-advisor.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/delt96/claude-jev-advisor/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/delt96/claude-jev-advisor#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"tsx": "^4.23.15",
|
|
46
|
+
"typescript": "^5.5.0"
|
|
47
|
+
}
|
|
48
|
+
}
|