@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,394 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/context/hook.ts
|
|
4
|
+
import os from "os";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
|
|
10
|
+
// src/config-shape.ts
|
|
11
|
+
var DEFAULT_CONFIG = {
|
|
12
|
+
lang: "ko",
|
|
13
|
+
keyFile: null,
|
|
14
|
+
display: "mod",
|
|
15
|
+
context: { enabled: true, minTokens: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
|
|
16
|
+
rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
|
|
17
|
+
};
|
|
18
|
+
var LANGS = ["ko", "en"];
|
|
19
|
+
var DISPLAYS = ["mod", "statusline", "message"];
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function mergeSection(defaults, raw) {
|
|
24
|
+
const out = { ...defaults };
|
|
25
|
+
if (!isRecord(raw)) return out;
|
|
26
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
27
|
+
if (typeof raw[key] === typeof value) out[key] = raw[key];
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function normalizeConfig(raw) {
|
|
32
|
+
const r = isRecord(raw) ? raw : {};
|
|
33
|
+
return {
|
|
34
|
+
lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
|
|
35
|
+
keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
|
|
36
|
+
display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
|
|
37
|
+
context: mergeSection(DEFAULT_CONFIG.context, r.context),
|
|
38
|
+
rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/paths.ts
|
|
43
|
+
import path from "path";
|
|
44
|
+
var claudeDir = (home) => path.join(home, ".claude");
|
|
45
|
+
var dataDir = (home) => path.join(claudeDir(home), "claude-jev-advisor");
|
|
46
|
+
var configPath = (home) => path.join(dataDir(home), "config.json");
|
|
47
|
+
var stateDir = (home) => path.join(dataDir(home), "state");
|
|
48
|
+
var logDir = (home) => path.join(dataDir(home), "log");
|
|
49
|
+
|
|
50
|
+
// src/config.ts
|
|
51
|
+
function readConfig(home) {
|
|
52
|
+
try {
|
|
53
|
+
return normalizeConfig(JSON.parse(fs.readFileSync(configPath(home), "utf8").replace(/^\uFEFF/, "")));
|
|
54
|
+
} catch {
|
|
55
|
+
return normalizeConfig({});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/display/line.ts
|
|
60
|
+
var PHRASES = {
|
|
61
|
+
ko: {
|
|
62
|
+
clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
|
|
63
|
+
compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
|
|
64
|
+
compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
|
|
65
|
+
},
|
|
66
|
+
en: {
|
|
67
|
+
clear: "Start fresh? /clear",
|
|
68
|
+
compact: "Wrap up what you have and continue? /compact",
|
|
69
|
+
compactLater: "When this work is done, wrap up and continue? /compact"
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
function formatSize(tokens) {
|
|
73
|
+
return `${Math.round(tokens / 1e3)}k`;
|
|
74
|
+
}
|
|
75
|
+
function remainingPct(size, threshold) {
|
|
76
|
+
return Math.max(0, Math.round((threshold - size) / threshold * 100));
|
|
77
|
+
}
|
|
78
|
+
function adviceLine({ size, threshold, judgment, config }) {
|
|
79
|
+
if (size === null) return "";
|
|
80
|
+
const k = formatSize(size);
|
|
81
|
+
const phrases = PHRASES[config.lang];
|
|
82
|
+
const counted = size >= config.context.minTokens ? judgment : null;
|
|
83
|
+
if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
|
|
84
|
+
const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
|
|
85
|
+
if (!counted) return head;
|
|
86
|
+
if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
|
|
87
|
+
return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
|
|
88
|
+
}
|
|
89
|
+
if (!counted) return k;
|
|
90
|
+
if (counted.phase === "working") return `\u{1F7E2} ${k}`;
|
|
91
|
+
if (counted.clear) return `\u{1F7E1} ${k} ${phrases.clear}`;
|
|
92
|
+
if (size >= config.context.compactMinTokens) return `\u{1F7E1} ${k} ${phrases.compact}`;
|
|
93
|
+
return `\u{1F7E2} ${k}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/jev.ts
|
|
97
|
+
import fs2 from "fs";
|
|
98
|
+
var JEV_URL = "https://api.typesafe.ai/v1/systemone";
|
|
99
|
+
var JEV_TIMEOUT_MS = 8e3;
|
|
100
|
+
var KEY_PREFIX = "TYPESAFE_API_KEY=";
|
|
101
|
+
function readJevKey(env, keyFile) {
|
|
102
|
+
const fromEnv = env.TYPESAFE_API_KEY?.trim();
|
|
103
|
+
if (fromEnv) return fromEnv;
|
|
104
|
+
if (!keyFile) return null;
|
|
105
|
+
let text;
|
|
106
|
+
try {
|
|
107
|
+
text = fs2.readFileSync(keyFile, "utf8");
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const line = text.replace(/^\uFEFF/, "").split(/\r?\n/).find((l) => l.startsWith(KEY_PREFIX));
|
|
112
|
+
const key = line?.slice(KEY_PREFIX.length).trim().replace(/^["']|["']$/g, "");
|
|
113
|
+
return key || null;
|
|
114
|
+
}
|
|
115
|
+
async function callJev(request, opts) {
|
|
116
|
+
const now = opts.now ?? Date.now;
|
|
117
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
118
|
+
const limit = opts.timeoutMs ?? JEV_TIMEOUT_MS;
|
|
119
|
+
const redact = (text) => text.split(opts.key).join("[redacted]");
|
|
120
|
+
const started = now();
|
|
121
|
+
const controller = new AbortController();
|
|
122
|
+
let timer;
|
|
123
|
+
const expired = new Promise((_resolve, reject) => {
|
|
124
|
+
timer = setTimeout(() => {
|
|
125
|
+
const err = new Error(`Jev did not answer within ${limit} ms`);
|
|
126
|
+
controller.abort(err);
|
|
127
|
+
reject(err);
|
|
128
|
+
}, limit);
|
|
129
|
+
});
|
|
130
|
+
let requestId = null;
|
|
131
|
+
const exchange = (async () => {
|
|
132
|
+
const res = await fetchFn(JEV_URL, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { Authorization: `Bearer ${opts.key}`, "Content-Type": "application/json" },
|
|
135
|
+
body: JSON.stringify({ model: "jev-latest", ...request }),
|
|
136
|
+
signal: controller.signal
|
|
137
|
+
});
|
|
138
|
+
requestId = res.headers.get("request-id") ?? res.headers.get("x-request-id");
|
|
139
|
+
return { res, text: await res.text() };
|
|
140
|
+
})();
|
|
141
|
+
exchange.catch(() => {
|
|
142
|
+
});
|
|
143
|
+
try {
|
|
144
|
+
const { res, text } = await Promise.race([exchange, expired]);
|
|
145
|
+
if (!res.ok) return { error: redact(`HTTP ${res.status}: ${text}`).slice(0, 300), requestId, ms: now() - started };
|
|
146
|
+
const body = JSON.parse(text);
|
|
147
|
+
const answers = {};
|
|
148
|
+
for (const [id, answer] of Object.entries(body.answers ?? {})) {
|
|
149
|
+
if (typeof answer?.noul === "number") answers[id] = answer.noul;
|
|
150
|
+
}
|
|
151
|
+
return { answers, model: typeof body.model === "string" ? body.model : null, requestId, ms: now() - started };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { error: redact(String(err?.message ?? err)).slice(0, 300), requestId, ms: now() - started };
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/context/files.ts
|
|
160
|
+
import fs3 from "fs";
|
|
161
|
+
import path3 from "path";
|
|
162
|
+
var SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
163
|
+
var RENAME_ATTEMPTS = 3;
|
|
164
|
+
var RETRYABLE_RENAME = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
165
|
+
function pause(ms) {
|
|
166
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
167
|
+
}
|
|
168
|
+
function statePath(home, sessionId) {
|
|
169
|
+
return SAFE_SESSION_ID.test(sessionId) ? path3.join(stateDir(home), `${sessionId}.json`) : null;
|
|
170
|
+
}
|
|
171
|
+
function writeState(home, state) {
|
|
172
|
+
const file = statePath(home, state.sessionId);
|
|
173
|
+
if (!file) return;
|
|
174
|
+
fs3.mkdirSync(path3.dirname(file), { recursive: true });
|
|
175
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
176
|
+
fs3.writeFileSync(temp, JSON.stringify(state));
|
|
177
|
+
try {
|
|
178
|
+
for (let attempt = 1; ; attempt++) {
|
|
179
|
+
try {
|
|
180
|
+
fs3.renameSync(temp, file);
|
|
181
|
+
return;
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (attempt >= RENAME_ATTEMPTS || !RETRYABLE_RENAME.has(err.code ?? "")) throw err;
|
|
184
|
+
pause(20 * attempt);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
fs3.rmSync(temp, { force: true });
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function removeState(home, sessionId) {
|
|
193
|
+
const file = statePath(home, sessionId);
|
|
194
|
+
if (file) fs3.rmSync(file, { force: true });
|
|
195
|
+
}
|
|
196
|
+
function logFile(home, now) {
|
|
197
|
+
return path3.join(logDir(home), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}.jsonl`);
|
|
198
|
+
}
|
|
199
|
+
function appendLog(home, now, record) {
|
|
200
|
+
const file = logFile(home, now);
|
|
201
|
+
fs3.mkdirSync(path3.dirname(file), { recursive: true });
|
|
202
|
+
fs3.appendFileSync(file, `${JSON.stringify({ at: now.toISOString(), ...record })}
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/context/judge.ts
|
|
207
|
+
var REPLY_EDGE_CHARS = 1500;
|
|
208
|
+
var QUESTIONS = {
|
|
209
|
+
unit_done: {
|
|
210
|
+
type: "noul",
|
|
211
|
+
instructions: "Has the work asked for in `recent_requests` reached a natural stopping point in `last_assistant_reply`?",
|
|
212
|
+
criteria: {
|
|
213
|
+
true: "The requested work is finished, verified or handed over; anything still mentioned is optional.",
|
|
214
|
+
false: "The work is still in progress: the reply asks a question it needs answered to continue, says what it will do next, or reports something still running or failing."
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
goal_done: {
|
|
218
|
+
type: "noul",
|
|
219
|
+
instructions: "Does `last_assistant_reply` wrap up the overall goal behind `recent_requests`, with no further steps on that goal left to take next?",
|
|
220
|
+
criteria: {
|
|
221
|
+
true: "The goal is finished: the reply hands over the result or reports a commit, release or completion, and plans or proposes no further work toward the same goal.",
|
|
222
|
+
false: "Steps toward the same goal remain: the next task of a plan, a proposed follow-up, or a review, test or decision the user still has to make."
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function cutReply(text, edge = REPLY_EDGE_CHARS) {
|
|
227
|
+
return text.length <= edge * 2 ? text : `${text.slice(0, edge)}
|
|
228
|
+
\u2026
|
|
229
|
+
${text.slice(-edge)}`;
|
|
230
|
+
}
|
|
231
|
+
function jevRequest(requests, lastReply) {
|
|
232
|
+
return {
|
|
233
|
+
state: { recent_requests: requests, last_assistant_reply: cutReply(lastReply) },
|
|
234
|
+
questions: { unit_done: QUESTIONS.unit_done, goal_done: QUESTIONS.goal_done }
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function judgmentFrom(answers, config) {
|
|
238
|
+
const unit = answers.unit_done;
|
|
239
|
+
if (typeof unit !== "number") return null;
|
|
240
|
+
if (unit < config.context.unitDoneYes) return { phase: "working", clear: false };
|
|
241
|
+
const goal = answers.goal_done;
|
|
242
|
+
return { phase: "unit_done", clear: typeof goal === "number" && goal >= config.context.goalDoneYes };
|
|
243
|
+
}
|
|
244
|
+
function hasBackgroundTasks(value) {
|
|
245
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
246
|
+
return typeof value === "object" && value !== null && Object.keys(value).length > 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/context/transcript.ts
|
|
250
|
+
import fs4 from "fs";
|
|
251
|
+
var TAIL_START_BYTES = 1024 * 1024;
|
|
252
|
+
var TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
253
|
+
var REQUEST_COUNT = 3;
|
|
254
|
+
var REQUEST_MAX_CHARS = 1e3;
|
|
255
|
+
var REQUEST_ORIGINS = /* @__PURE__ */ new Set(["human", "channel"]);
|
|
256
|
+
function isRecord2(value) {
|
|
257
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
258
|
+
}
|
|
259
|
+
function parseLines(text) {
|
|
260
|
+
const entries = [];
|
|
261
|
+
for (const line of text.split("\n")) {
|
|
262
|
+
if (!line.trim()) continue;
|
|
263
|
+
try {
|
|
264
|
+
const value = JSON.parse(line);
|
|
265
|
+
if (isRecord2(value)) entries.push(value);
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return entries;
|
|
270
|
+
}
|
|
271
|
+
function readTail(file, bytes) {
|
|
272
|
+
const fd = fs4.openSync(file, "r");
|
|
273
|
+
try {
|
|
274
|
+
const size = fs4.fstatSync(fd).size;
|
|
275
|
+
const length = Math.min(size, bytes);
|
|
276
|
+
const buffer = Buffer.alloc(length);
|
|
277
|
+
fs4.readSync(fd, buffer, 0, length, size - length);
|
|
278
|
+
let text = buffer.toString("utf8");
|
|
279
|
+
if (length < size) text = text.slice(text.indexOf("\n") + 1);
|
|
280
|
+
return { entries: parseLines(text), whole: length === size };
|
|
281
|
+
} finally {
|
|
282
|
+
fs4.closeSync(fd);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function contextSize(entries) {
|
|
286
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
287
|
+
const entry = entries[i];
|
|
288
|
+
if (entry.type === "system" && entry.subtype === "compact_boundary") return 0;
|
|
289
|
+
if (entry.type !== "assistant" || entry.isSidechain === true || !isRecord2(entry.message)) continue;
|
|
290
|
+
const { model, usage } = entry.message;
|
|
291
|
+
if (model === "<synthetic>" || !isRecord2(usage)) continue;
|
|
292
|
+
const count = (key) => typeof usage[key] === "number" ? usage[key] : 0;
|
|
293
|
+
return count("input_tokens") + count("cache_read_input_tokens") + count("cache_creation_input_tokens");
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
function requestText(content) {
|
|
298
|
+
if (typeof content === "string") return content;
|
|
299
|
+
if (!Array.isArray(content)) return "";
|
|
300
|
+
return content.filter((block) => isRecord2(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
301
|
+
}
|
|
302
|
+
function recentRequests(entries, count = REQUEST_COUNT, maxChars = REQUEST_MAX_CHARS) {
|
|
303
|
+
const found = [];
|
|
304
|
+
for (let i = entries.length - 1; i >= 0 && found.length < count; i--) {
|
|
305
|
+
const entry = entries[i];
|
|
306
|
+
if (entry.type !== "user" || entry.isSidechain === true || !isRecord2(entry.origin) || !REQUEST_ORIGINS.has(String(entry.origin.kind))) continue;
|
|
307
|
+
const text = requestText(isRecord2(entry.message) ? entry.message.content : void 0).trim();
|
|
308
|
+
if (text) found.push(text.slice(0, maxChars));
|
|
309
|
+
}
|
|
310
|
+
return found.reverse();
|
|
311
|
+
}
|
|
312
|
+
function readTranscript(file, limits = { start: TAIL_START_BYTES, max: TAIL_MAX_BYTES }) {
|
|
313
|
+
for (let bytes = limits.start; ; bytes *= 2) {
|
|
314
|
+
const { entries, whole } = readTail(file, Math.min(bytes, limits.max));
|
|
315
|
+
const size = contextSize(entries);
|
|
316
|
+
const requests = recentRequests(entries);
|
|
317
|
+
if (size !== null && requests.length >= REQUEST_COUNT || whole || bytes >= limits.max) return { size, requests };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/context/run.ts
|
|
322
|
+
var ADVICE = /\/(clear|compact)$/;
|
|
323
|
+
function factsOf(transcriptPath) {
|
|
324
|
+
if (!transcriptPath) return { size: null, requests: [] };
|
|
325
|
+
try {
|
|
326
|
+
return readTranscript(transcriptPath);
|
|
327
|
+
} catch {
|
|
328
|
+
return { size: null, requests: [] };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function runContextHook(event, raw, deps) {
|
|
332
|
+
const config = readConfig(deps.home);
|
|
333
|
+
if (!config.context.enabled) return null;
|
|
334
|
+
const input = JSON.parse(raw);
|
|
335
|
+
if (typeof input !== "object" || input === null || typeof input.session_id !== "string") return null;
|
|
336
|
+
const sessionId = input.session_id;
|
|
337
|
+
if (event === "session-end") {
|
|
338
|
+
removeState(deps.home, sessionId);
|
|
339
|
+
appendLog(deps.home, deps.now(), { helper: "context", event: "session_end", sessionId, reason: typeof input.reason === "string" ? input.reason : null });
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
if (input.stop_hook_active === true) return null;
|
|
343
|
+
const transcriptPath = typeof input.transcript_path === "string" ? input.transcript_path : null;
|
|
344
|
+
const { size, requests } = factsOf(transcriptPath);
|
|
345
|
+
let judgment = null;
|
|
346
|
+
let reason = null;
|
|
347
|
+
let jev = null;
|
|
348
|
+
if (size === null) {
|
|
349
|
+
reason = "unknown_size";
|
|
350
|
+
} else if (size < config.context.minTokens) {
|
|
351
|
+
reason = "below_min";
|
|
352
|
+
} else if (hasBackgroundTasks(input.background_tasks)) {
|
|
353
|
+
reason = "background_tasks";
|
|
354
|
+
judgment = { phase: "working", clear: false };
|
|
355
|
+
} else {
|
|
356
|
+
const key = readJevKey(deps.env, config.keyFile);
|
|
357
|
+
if (!key) {
|
|
358
|
+
reason = "no_key";
|
|
359
|
+
} else {
|
|
360
|
+
const request = jevRequest(requests, typeof input.last_assistant_message === "string" ? input.last_assistant_message : "");
|
|
361
|
+
const result = await callJev(request, { key, fetchFn: deps.fetchFn });
|
|
362
|
+
jev = { state: request.state, ...result };
|
|
363
|
+
if ("error" in result) {
|
|
364
|
+
reason = "jev_error";
|
|
365
|
+
} else {
|
|
366
|
+
judgment = judgmentFrom(result.answers, config);
|
|
367
|
+
if (!judgment) reason = "jev_no_answer";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const at = deps.now();
|
|
372
|
+
const state = { sessionId, at: at.getTime(), size, judgment };
|
|
373
|
+
try {
|
|
374
|
+
writeState(deps.home, state);
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
appendLog(deps.home, at, { helper: "context", event: "stop", sessionId, transcriptPath, size, reason, judgment, jev });
|
|
378
|
+
if (config.display !== "message") return null;
|
|
379
|
+
const line = adviceLine({ size, threshold: null, judgment, config });
|
|
380
|
+
return ADVICE.test(line) ? JSON.stringify({ systemMessage: line }) : null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/context/hook.ts
|
|
384
|
+
async function main() {
|
|
385
|
+
const event = process.argv[2];
|
|
386
|
+
if (event !== "stop" && event !== "session-end") return;
|
|
387
|
+
let raw = "";
|
|
388
|
+
process.stdin.setEncoding("utf8");
|
|
389
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
390
|
+
const out = await runContextHook(event, raw, { home: os.homedir(), env: process.env, now: () => /* @__PURE__ */ new Date() });
|
|
391
|
+
if (out) process.stdout.write(out);
|
|
392
|
+
}
|
|
393
|
+
main().catch(() => {
|
|
394
|
+
});
|