@delt/claude-jev-advisor 0.1.2 → 0.3.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 +61 -30
- package/dist/cli.js +262 -35
- package/dist/context-hook.js +111 -44
- package/dist/rm-hook.js +1474 -93
- package/dist/statusline.js +84 -14
- package/mod/.claude-plugin/plugin.json +14 -14
- package/mod/hooks/register.js +15 -6
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import os from "os";
|
|
5
|
-
import
|
|
5
|
+
import path10 from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/cli/prompt.ts
|
|
@@ -71,21 +71,23 @@ function terminalPrompt(terminal = PROCESS_TERMINAL) {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// src/cli/run.ts
|
|
74
|
-
import
|
|
75
|
-
import
|
|
74
|
+
import fs9 from "fs";
|
|
75
|
+
import path9 from "path";
|
|
76
76
|
|
|
77
77
|
// src/config.ts
|
|
78
78
|
import fs from "fs";
|
|
79
79
|
import path2 from "path";
|
|
80
80
|
|
|
81
81
|
// src/config-shape.ts
|
|
82
|
+
var CONFIG_VERSION = 2;
|
|
82
83
|
var DEFAULT_CONFIG = {
|
|
83
84
|
lang: "ko",
|
|
84
85
|
keyFile: null,
|
|
85
86
|
display: "mod",
|
|
86
|
-
context: { enabled: true, minTokens:
|
|
87
|
+
context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
|
|
87
88
|
rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
|
|
88
89
|
};
|
|
90
|
+
var LEGACY_CONTEXT_DEFAULTS = { minTokens: 1e5, compactMinTokens: 2e5, unitDoneYes: 0.7 };
|
|
89
91
|
var LANGS = ["ko", "en"];
|
|
90
92
|
var DISPLAYS = ["mod", "statusline", "message"];
|
|
91
93
|
function isRecord(value) {
|
|
@@ -99,16 +101,33 @@ function mergeSection(defaults, raw) {
|
|
|
99
101
|
}
|
|
100
102
|
return out;
|
|
101
103
|
}
|
|
104
|
+
function withoutLegacyDefaults(context) {
|
|
105
|
+
if (!isRecord(context)) return context;
|
|
106
|
+
return Object.fromEntries(Object.entries(context).filter(([key, value]) => LEGACY_CONTEXT_DEFAULTS[key] !== value));
|
|
107
|
+
}
|
|
102
108
|
function normalizeConfig(raw) {
|
|
103
109
|
const r = isRecord(raw) ? raw : {};
|
|
104
110
|
return {
|
|
105
111
|
lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
|
|
106
112
|
keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
|
|
107
113
|
display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
|
|
108
|
-
context: mergeSection(DEFAULT_CONFIG.context, r.context),
|
|
114
|
+
context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
|
|
109
115
|
rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
|
|
110
116
|
};
|
|
111
117
|
}
|
|
118
|
+
function changedValues(values, defaults) {
|
|
119
|
+
return Object.fromEntries(Object.entries(values).filter(([key, value]) => value !== defaults[key]));
|
|
120
|
+
}
|
|
121
|
+
function configToSave(config) {
|
|
122
|
+
const { context, rm, ...top } = config;
|
|
123
|
+
const { context: defaultContext, rm: defaultRm, ...defaultTop } = DEFAULT_CONFIG;
|
|
124
|
+
const out = { version: CONFIG_VERSION, ...changedValues(top, defaultTop) };
|
|
125
|
+
const changedContext = changedValues(context, defaultContext);
|
|
126
|
+
const changedRm = changedValues(rm, defaultRm);
|
|
127
|
+
if (Object.keys(changedContext).length) out.context = changedContext;
|
|
128
|
+
if (Object.keys(changedRm).length) out.rm = changedRm;
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
112
131
|
|
|
113
132
|
// src/paths.ts
|
|
114
133
|
import path from "path";
|
|
@@ -118,6 +137,7 @@ var configPath = (home) => path.join(dataDir(home), "config.json");
|
|
|
118
137
|
var settingsPath = (home) => path.join(claudeDir(home), "settings.json");
|
|
119
138
|
var backupsDir = (home) => path.join(claudeDir(home), "backups");
|
|
120
139
|
var stateDir = (home) => path.join(dataDir(home), "state");
|
|
140
|
+
var logDir = (home) => path.join(dataDir(home), "log");
|
|
121
141
|
var statusLineBeforePath = (home) => path.join(dataDir(home), "statusline-before.json");
|
|
122
142
|
var savedKeyPath = (home) => path.join(dataDir(home), "jev-key.env");
|
|
123
143
|
|
|
@@ -132,7 +152,7 @@ function readConfig(home) {
|
|
|
132
152
|
function writeConfig(home, config) {
|
|
133
153
|
const file = configPath(home);
|
|
134
154
|
fs.mkdirSync(path2.dirname(file), { recursive: true });
|
|
135
|
-
fs.writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
155
|
+
fs.writeFileSync(file, `${JSON.stringify(configToSave(config), null, 2)}
|
|
136
156
|
`);
|
|
137
157
|
}
|
|
138
158
|
function updateConfig(home, change) {
|
|
@@ -144,7 +164,7 @@ function updateConfig(home, change) {
|
|
|
144
164
|
// src/features.ts
|
|
145
165
|
var FEATURES = ["rm", "context"];
|
|
146
166
|
var HOOK_SPECS = {
|
|
147
|
-
rm: [{ event: "PreToolUse", matcher: "Bash", script: "rm-hook.js", args: [] }],
|
|
167
|
+
rm: [{ event: "PreToolUse", matcher: "Bash|PowerShell", script: "rm-hook.js", args: [] }],
|
|
148
168
|
context: [
|
|
149
169
|
{ event: "Stop", script: "context-hook.js", args: ["stop"] },
|
|
150
170
|
{ event: "SessionEnd", script: "context-hook.js", args: ["session-end"] }
|
|
@@ -247,8 +267,8 @@ function withoutModDisplay(settings, delimiter) {
|
|
|
247
267
|
const dirs = env?.[PLUGIN_DIRS_ENV];
|
|
248
268
|
if (env && typeof dirs === "string") {
|
|
249
269
|
const all = dirs.split(delimiter);
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
if (all.some((dir) => MOD_DIR_PATTERN.test(forwardSlashes2(dir)))) {
|
|
271
|
+
const kept = all.filter((dir) => dir !== "" && !MOD_DIR_PATTERN.test(forwardSlashes2(dir)));
|
|
252
272
|
const { [PLUGIN_DIRS_ENV]: _ours, ...otherEnv } = env;
|
|
253
273
|
const nextEnv = kept.length ? { ...env, [PLUGIN_DIRS_ENV]: kept.join(delimiter) } : otherEnv;
|
|
254
274
|
next = Object.keys(nextEnv).length ? { ...next, env: nextEnv } : withoutKey(next, "env");
|
|
@@ -264,11 +284,11 @@ function withoutModDisplay(settings, delimiter) {
|
|
|
264
284
|
function withModDisplay(settings, modDir, dataDirPath, delimiter) {
|
|
265
285
|
const base = withoutModDisplay(settings, delimiter);
|
|
266
286
|
const env = isRecord2(base.env) ? base.env : {};
|
|
267
|
-
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string"
|
|
287
|
+
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV].split(delimiter).filter(Boolean) : [];
|
|
268
288
|
const configs = isRecord2(base.pluginConfigs) ? base.pluginConfigs : {};
|
|
269
289
|
return {
|
|
270
290
|
...base,
|
|
271
|
-
env: { ...env, [PLUGIN_DIRS_ENV]:
|
|
291
|
+
env: { ...env, [PLUGIN_DIRS_ENV]: [...dirs, forwardSlashes2(modDir)].join(delimiter) },
|
|
272
292
|
pluginConfigs: { ...configs, [MOD_PLUGIN_NAME]: { options: { dataDir: forwardSlashes2(dataDirPath) } } }
|
|
273
293
|
};
|
|
274
294
|
}
|
|
@@ -548,10 +568,42 @@ async function promptForKey(prompt, check) {
|
|
|
548
568
|
}
|
|
549
569
|
}
|
|
550
570
|
|
|
551
|
-
// src/
|
|
552
|
-
import
|
|
571
|
+
// src/report.ts
|
|
572
|
+
import fs7 from "fs";
|
|
553
573
|
import path7 from "path";
|
|
554
574
|
|
|
575
|
+
// src/context/transcript.ts
|
|
576
|
+
import fs6 from "fs";
|
|
577
|
+
var TAIL_START_BYTES = 1024 * 1024;
|
|
578
|
+
var TAIL_MAX_BYTES = 8 * 1024 * 1024;
|
|
579
|
+
var REQUEST_ORIGINS = /* @__PURE__ */ new Set(["human", "channel", "peer"]);
|
|
580
|
+
function isRecord3(value) {
|
|
581
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
582
|
+
}
|
|
583
|
+
function parseLines(text) {
|
|
584
|
+
const entries = [];
|
|
585
|
+
for (const line of text.split("\n")) {
|
|
586
|
+
if (!line.trim()) continue;
|
|
587
|
+
try {
|
|
588
|
+
const value = JSON.parse(line);
|
|
589
|
+
if (isRecord3(value)) entries.push(value);
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return entries;
|
|
594
|
+
}
|
|
595
|
+
function messageText(content) {
|
|
596
|
+
if (typeof content === "string") return content;
|
|
597
|
+
if (!Array.isArray(content)) return "";
|
|
598
|
+
return content.filter((block) => isRecord3(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
|
|
599
|
+
}
|
|
600
|
+
function isRequest(entry) {
|
|
601
|
+
return entry.type === "user" && entry.isSidechain !== true && isRecord3(entry.origin) && REQUEST_ORIGINS.has(String(entry.origin.kind));
|
|
602
|
+
}
|
|
603
|
+
function requestText(entry) {
|
|
604
|
+
return messageText(isRecord3(entry.message) ? entry.message.content : void 0).trim();
|
|
605
|
+
}
|
|
606
|
+
|
|
555
607
|
// src/display/line.ts
|
|
556
608
|
var PHRASES = {
|
|
557
609
|
ko: {
|
|
@@ -565,7 +617,7 @@ var PHRASES = {
|
|
|
565
617
|
compactLater: "When this work is done, wrap up and continue? /compact"
|
|
566
618
|
}
|
|
567
619
|
};
|
|
568
|
-
function
|
|
620
|
+
function isRecord4(value) {
|
|
569
621
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
570
622
|
}
|
|
571
623
|
function formatSize(tokens) {
|
|
@@ -574,6 +626,11 @@ function formatSize(tokens) {
|
|
|
574
626
|
function remainingPct(size, threshold) {
|
|
575
627
|
return Math.max(0, Math.round((threshold - size) / threshold * 100));
|
|
576
628
|
}
|
|
629
|
+
function adviceKind(size, judgment, config) {
|
|
630
|
+
if (size === null || size < config.context.minTokens || !judgment || judgment.phase === "working") return null;
|
|
631
|
+
if (judgment.clear) return "clear";
|
|
632
|
+
return size >= config.context.compactMinTokens ? "compact" : null;
|
|
633
|
+
}
|
|
577
634
|
function adviceLine({ size, threshold, judgment, config }) {
|
|
578
635
|
if (size === null) return "";
|
|
579
636
|
const k = formatSize(size);
|
|
@@ -586,41 +643,181 @@ function adviceLine({ size, threshold, judgment, config }) {
|
|
|
586
643
|
return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
|
|
587
644
|
}
|
|
588
645
|
if (!counted) return k;
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (size >= config.context.compactMinTokens) return `\u{1F7E1} ${k} ${phrases.compact}`;
|
|
592
|
-
return `\u{1F7E2} ${k}`;
|
|
646
|
+
const kind = adviceKind(size, counted, config);
|
|
647
|
+
return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
|
|
593
648
|
}
|
|
594
649
|
function parseState(raw) {
|
|
595
|
-
if (!
|
|
650
|
+
if (!isRecord4(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
|
|
596
651
|
const j = raw.judgment;
|
|
597
|
-
const judgment =
|
|
652
|
+
const judgment = isRecord4(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
|
|
598
653
|
return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
|
|
599
654
|
}
|
|
600
655
|
|
|
656
|
+
// src/report.ts
|
|
657
|
+
var FOLLOW_REQUESTS = 3;
|
|
658
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
659
|
+
function isRecord5(value) {
|
|
660
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
661
|
+
}
|
|
662
|
+
function monthFiles(home, since, now) {
|
|
663
|
+
const files = [];
|
|
664
|
+
for (let d = new Date(since.getFullYear(), since.getMonth(), 1); d <= now; d = new Date(d.getFullYear(), d.getMonth() + 1, 1)) {
|
|
665
|
+
files.push(path7.join(logDir(home), `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}.jsonl`));
|
|
666
|
+
}
|
|
667
|
+
return files;
|
|
668
|
+
}
|
|
669
|
+
function readLogRecords(home, since, now) {
|
|
670
|
+
const records = [];
|
|
671
|
+
for (const file of monthFiles(home, since, now)) {
|
|
672
|
+
let text;
|
|
673
|
+
try {
|
|
674
|
+
text = fs7.readFileSync(file, "utf8");
|
|
675
|
+
} catch {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
for (const record of parseLines(text)) {
|
|
679
|
+
const at = typeof record.at === "string" ? Date.parse(record.at) : NaN;
|
|
680
|
+
if (at >= since.getTime() && at <= now.getTime()) records.push(record);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return records;
|
|
684
|
+
}
|
|
685
|
+
function adviceOf(record, config) {
|
|
686
|
+
if ("advice" in record) return record.advice === "clear" || record.advice === "compact" ? record.advice : null;
|
|
687
|
+
const state = parseState({ sessionId: record.sessionId, at: 0, size: record.size, judgment: record.judgment });
|
|
688
|
+
return state ? adviceKind(state.size, state.judgment, config) : null;
|
|
689
|
+
}
|
|
690
|
+
function outcomeAfter(entries, at, endedByClear) {
|
|
691
|
+
let requests = 0;
|
|
692
|
+
for (const entry of entries) {
|
|
693
|
+
const time = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
|
|
694
|
+
if (!(time > at)) continue;
|
|
695
|
+
if (entry.type === "system" && entry.subtype === "compact_boundary") {
|
|
696
|
+
const trigger = isRecord5(entry.compactMetadata) ? entry.compactMetadata.trigger : void 0;
|
|
697
|
+
return { kind: trigger === "auto" ? "auto_compact" : "compact" };
|
|
698
|
+
}
|
|
699
|
+
if (isRequest(entry) && requestText(entry)) {
|
|
700
|
+
requests += 1;
|
|
701
|
+
if (requests >= FOLLOW_REQUESTS) return { kind: "kept", requests };
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return endedByClear ? { kind: "clear" } : { kind: "kept", requests };
|
|
705
|
+
}
|
|
706
|
+
function adviceRows(home, now, days) {
|
|
707
|
+
const config = readConfig(home);
|
|
708
|
+
const records = readLogRecords(home, new Date(now.getTime() - days * DAY_MS), now);
|
|
709
|
+
const cleared = new Set(records.filter((r) => r.event === "session_end" && r.reason === "clear").map((r) => r.sessionId));
|
|
710
|
+
const transcripts = /* @__PURE__ */ new Map();
|
|
711
|
+
const entriesOf = (file) => {
|
|
712
|
+
if (typeof file !== "string") return null;
|
|
713
|
+
if (!transcripts.has(file)) {
|
|
714
|
+
try {
|
|
715
|
+
transcripts.set(file, parseLines(fs7.readFileSync(file, "utf8")));
|
|
716
|
+
} catch {
|
|
717
|
+
transcripts.set(file, null);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return transcripts.get(file) ?? null;
|
|
721
|
+
};
|
|
722
|
+
const rows = [];
|
|
723
|
+
for (const record of records) {
|
|
724
|
+
if (record.helper !== "context" || record.event !== "stop" || typeof record.sessionId !== "string" || typeof record.size !== "number") continue;
|
|
725
|
+
const advice = adviceOf(record, config);
|
|
726
|
+
if (!advice) continue;
|
|
727
|
+
const at = new Date(record.at);
|
|
728
|
+
const entries = entriesOf(record.transcriptPath);
|
|
729
|
+
const jev = isRecord5(record.jev) ? record.jev : {};
|
|
730
|
+
const state = isRecord5(jev.state) ? jev.state : {};
|
|
731
|
+
const requests = Array.isArray(state.recent_requests) ? state.recent_requests : [];
|
|
732
|
+
const last = requests[requests.length - 1];
|
|
733
|
+
rows.push({
|
|
734
|
+
at,
|
|
735
|
+
sessionId: record.sessionId,
|
|
736
|
+
size: record.size,
|
|
737
|
+
advice,
|
|
738
|
+
answers: isRecord5(jev.answers) ? jev.answers : null,
|
|
739
|
+
request: typeof last === "string" ? last : "",
|
|
740
|
+
outcome: entries ? outcomeAfter(entries, at.getTime(), cleared.has(record.sessionId)) : { kind: "unknown" }
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
return rows;
|
|
744
|
+
}
|
|
745
|
+
function localTime(date) {
|
|
746
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
747
|
+
return `${date.getFullYear()}-${p(date.getMonth() + 1)}-${p(date.getDate())} ${p(date.getHours())}:${p(date.getMinutes())}`;
|
|
748
|
+
}
|
|
749
|
+
function outcomeText(outcome) {
|
|
750
|
+
switch (outcome.kind) {
|
|
751
|
+
case "compact":
|
|
752
|
+
return "compacted";
|
|
753
|
+
case "clear":
|
|
754
|
+
return "cleared";
|
|
755
|
+
case "auto_compact":
|
|
756
|
+
return "auto-compacted";
|
|
757
|
+
case "kept":
|
|
758
|
+
return outcome.requests >= FOLLOW_REQUESTS ? `kept going, ${FOLLOW_REQUESTS}+ requests` : `kept going, ${outcome.requests} request${outcome.requests === 1 ? "" : "s"}`;
|
|
759
|
+
default:
|
|
760
|
+
return "transcript not found";
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function answersText(answers) {
|
|
764
|
+
if (!answers) return "";
|
|
765
|
+
return Object.entries(answers).filter(([, p]) => typeof p === "number").map(([id, p]) => `${id.replace(/_done$/, "")} ${p.toFixed(2)}`).join(" ");
|
|
766
|
+
}
|
|
767
|
+
function rmLines(home, now, days) {
|
|
768
|
+
const records = readLogRecords(home, new Date(now.getTime() - days * DAY_MS), now).filter((r) => r.helper === "rm");
|
|
769
|
+
const passed = records.filter((r) => r.decision === "pass");
|
|
770
|
+
const lines = [`rm deletions Jev let through: ${passed.length} (asked anyway after a Jev check: ${records.length - passed.length})`];
|
|
771
|
+
for (const record of passed) {
|
|
772
|
+
const targets = Array.isArray(record.targets) ? record.targets.filter(isRecord5) : [];
|
|
773
|
+
const listed = targets.map((t) => `${String(t.shown)} (${typeof t.p === "number" ? t.p.toFixed(2) : "?"})`).join(", ");
|
|
774
|
+
lines.push(` ${localTime(new Date(record.at))} ${String(record.sessionId ?? "").slice(0, 8)} ${listed}`);
|
|
775
|
+
}
|
|
776
|
+
return lines;
|
|
777
|
+
}
|
|
778
|
+
function reportLines(home, now, days) {
|
|
779
|
+
const rows = adviceRows(home, now, days);
|
|
780
|
+
const lines = [`Advice shown in the last ${days} day${days === 1 ? "" : "s"}: ${rows.length}`];
|
|
781
|
+
for (const row of rows) {
|
|
782
|
+
const request = row.request.replace(/\s+/g, " ").trim().slice(0, 40);
|
|
783
|
+
lines.push(` ${localTime(row.at)} ${row.sessionId.slice(0, 8)} ${formatSize(row.size)} /${row.advice} ${answersText(row.answers)} "${request}" -> ${outcomeText(row.outcome)}`);
|
|
784
|
+
}
|
|
785
|
+
for (const advice of ["clear", "compact"]) {
|
|
786
|
+
const mine = rows.filter((r) => r.advice === advice);
|
|
787
|
+
if (!mine.length) continue;
|
|
788
|
+
const count = (kind) => mine.filter((r) => r.outcome.kind === kind).length;
|
|
789
|
+
lines.push(
|
|
790
|
+
`/${advice}: ${mine.length} - compacted ${count("compact")}, cleared ${count("clear")}, auto-compacted ${count("auto_compact")}, kept going ${count("kept")}, unknown ${count("unknown")}`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return [...lines, ...rmLines(home, now, days)];
|
|
794
|
+
}
|
|
795
|
+
|
|
601
796
|
// src/status.ts
|
|
797
|
+
import fs8 from "fs";
|
|
798
|
+
import path8 from "path";
|
|
602
799
|
var SCRIPT_IN_COMMAND = /^node "([^"]+)"/;
|
|
603
|
-
function
|
|
800
|
+
function isRecord6(value) {
|
|
604
801
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
605
802
|
}
|
|
606
803
|
function displaySetUp(settings, config) {
|
|
607
804
|
if (config.display === "message") return true;
|
|
608
805
|
if (config.display === "statusline") return isOurStatusLine(settings.statusLine);
|
|
609
|
-
const env =
|
|
806
|
+
const env = isRecord6(settings.env) ? settings.env : {};
|
|
610
807
|
const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV] : "";
|
|
611
|
-
const hasDir = dirs.split(
|
|
612
|
-
return hasDir &&
|
|
808
|
+
const hasDir = dirs.split(path8.delimiter).some((dir) => MOD_DIR_PATTERN.test(dir.replace(/\\/g, "/")));
|
|
809
|
+
return hasDir && isRecord6(settings.pluginConfigs) && MOD_PLUGIN_NAME in settings.pluginConfigs;
|
|
613
810
|
}
|
|
614
811
|
function newestState(home) {
|
|
615
812
|
let newest = null;
|
|
616
813
|
try {
|
|
617
|
-
for (const name of
|
|
814
|
+
for (const name of fs8.readdirSync(stateDir(home))) {
|
|
618
815
|
if (!name.endsWith(".json")) continue;
|
|
619
|
-
const file =
|
|
620
|
-
const mtime =
|
|
816
|
+
const file = path8.join(stateDir(home), name);
|
|
817
|
+
const mtime = fs8.statSync(file).mtimeMs;
|
|
621
818
|
if (!newest || mtime > newest.mtime) newest = { file, mtime };
|
|
622
819
|
}
|
|
623
|
-
return newest ? parseState(JSON.parse(
|
|
820
|
+
return newest ? parseState(JSON.parse(fs8.readFileSync(newest.file, "utf8"))) : null;
|
|
624
821
|
} catch {
|
|
625
822
|
return null;
|
|
626
823
|
}
|
|
@@ -630,7 +827,8 @@ function contextLines(home, settings, config, env) {
|
|
|
630
827
|
const lines = [
|
|
631
828
|
` display: ${config.display}${displaySetUp(settings, config) ? "" : ` (not set up in settings.json - run "claude-jev-advisor install context --display ${config.display}")`}`,
|
|
632
829
|
` key: ${readJevKey(env, config.keyFile) ? "set" : "missing"}`,
|
|
633
|
-
` thresholds: judge from ${formatSize(c.minTokens)}, /compact from ${formatSize(c.compactMinTokens)}, red at ${c.redRemainingPct}% left
|
|
830
|
+
` thresholds: judge from ${formatSize(c.minTokens)}, /compact from ${formatSize(c.compactMinTokens)}, red at ${c.redRemainingPct}% left`,
|
|
831
|
+
` Jev: work done from ${c.unitDoneYes}, stage done (/clear) from ${c.phaseDoneYes}`
|
|
634
832
|
];
|
|
635
833
|
const last = newestState(home);
|
|
636
834
|
if (last) {
|
|
@@ -639,6 +837,19 @@ function contextLines(home, settings, config, env) {
|
|
|
639
837
|
}
|
|
640
838
|
return lines;
|
|
641
839
|
}
|
|
840
|
+
function rmJevLine(config, env) {
|
|
841
|
+
if (!config.rm.jev) return " jev: off";
|
|
842
|
+
const key = readJevKey(env, config.keyFile) ? "" : " (no key - every real file asks)";
|
|
843
|
+
return ` jev: on, lifts the ask from ${config.rm.throwawayYes}${key}`;
|
|
844
|
+
}
|
|
845
|
+
function coversPowerShell(matcher) {
|
|
846
|
+
if (!matcher || matcher === "*") return true;
|
|
847
|
+
try {
|
|
848
|
+
return new RegExp(`^(?:${matcher})$`).test("PowerShell");
|
|
849
|
+
} catch {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
642
853
|
function statusLines(home, env = process.env) {
|
|
643
854
|
const lines = [`settings: ${settingsPath(home)}`, `config: ${configPath(home)}`];
|
|
644
855
|
let settings;
|
|
@@ -658,11 +869,17 @@ function statusLines(home, env = process.env) {
|
|
|
658
869
|
lines.push(`${feature}: installed, ${state}`);
|
|
659
870
|
for (const c of commands) {
|
|
660
871
|
const script = SCRIPT_IN_COMMAND.exec(c.command)?.[1];
|
|
661
|
-
if (script && !
|
|
872
|
+
if (script && !fs8.existsSync(script)) {
|
|
662
873
|
lines.push(` broken: ${c.event} hook points to a missing file (${script}) - run "claude-jev-advisor install ${feature}" again, or "claude-jev-advisor uninstall ${feature}"`);
|
|
663
874
|
}
|
|
664
875
|
}
|
|
665
876
|
if (feature === "context") lines.push(...contextLines(home, settings, config, env));
|
|
877
|
+
if (feature === "rm") {
|
|
878
|
+
lines.push(rmJevLine(config, env));
|
|
879
|
+
if (!commands.some((c) => coversPowerShell(c.matcher))) {
|
|
880
|
+
lines.push(' Bash only: run "claude-jev-advisor install rm" to cover PowerShell');
|
|
881
|
+
}
|
|
882
|
+
}
|
|
666
883
|
}
|
|
667
884
|
if (findCommands(settings, LEGACY_RM_GUARD).length) {
|
|
668
885
|
lines.push('legacy rm-guard hook (~/.claude/hooks/rm-guard) is still registered - "claude-jev-advisor install rm" replaces it');
|
|
@@ -681,6 +898,7 @@ var USAGE = [
|
|
|
681
898
|
" off [rm] [context] switch helpers off (open sessions too)",
|
|
682
899
|
" status show what is registered and switched on",
|
|
683
900
|
" key ask for the TypeSafe API key, check it and save it",
|
|
901
|
+
" report [--days 7] list the /compact and /clear advice shown and what followed",
|
|
684
902
|
"",
|
|
685
903
|
"Options for install: --lang ko|en --key-file <path> --display mod|statusline|message",
|
|
686
904
|
'The display is set up when context is installed: "install context --display <mode>" switches it.',
|
|
@@ -688,6 +906,7 @@ var USAGE = [
|
|
|
688
906
|
'Run "claude-jev-advisor uninstall" before "npm rm -g": npm does not run uninstall scripts.'
|
|
689
907
|
].join("\n");
|
|
690
908
|
var INSTALL_OPTIONS = ["lang", "key-file", "display"];
|
|
909
|
+
var REPORT_DAYS = 7;
|
|
691
910
|
var LANGS2 = ["ko", "en"];
|
|
692
911
|
var DISPLAYS2 = ["mod", "statusline", "message"];
|
|
693
912
|
function parse(argv) {
|
|
@@ -711,7 +930,12 @@ function parse(argv) {
|
|
|
711
930
|
}
|
|
712
931
|
function checkOptions(command, options) {
|
|
713
932
|
const names = Object.keys(options);
|
|
714
|
-
if (command
|
|
933
|
+
if (command === "report") {
|
|
934
|
+
const unknown2 = names.find((n) => n !== "days");
|
|
935
|
+
if (unknown2) return `unknown option --${unknown2}`;
|
|
936
|
+
return options.days === void 0 || /^[1-9]\d{0,3}$/.test(options.days) ? null : "--days takes a whole number of days from 1";
|
|
937
|
+
}
|
|
938
|
+
if (command !== "install") return names.length ? `options are only for install and report (got --${names[0]})` : null;
|
|
715
939
|
const unknown = names.find((n) => !INSTALL_OPTIONS.includes(n));
|
|
716
940
|
if (unknown) return `unknown option --${unknown}`;
|
|
717
941
|
if (options.lang !== void 0 && !LANGS2.includes(options.lang)) return "--lang takes ko or en";
|
|
@@ -728,7 +952,7 @@ async function askAndSaveKey(io, prompt) {
|
|
|
728
952
|
const file = saveJevKey(io.home, key);
|
|
729
953
|
updateConfig(io.home, (c) => ({ ...c, keyFile: file }));
|
|
730
954
|
io.out(`Saved the key to ${file}`);
|
|
731
|
-
if (before &&
|
|
955
|
+
if (before && path9.resolve(before) !== path9.resolve(file)) io.out(`The config now uses this file instead of ${before}.`);
|
|
732
956
|
if ((io.env ?? process.env).TYPESAFE_API_KEY?.trim()) io.out("TYPESAFE_API_KEY is set in your environment; the hooks use it before the saved key.");
|
|
733
957
|
}
|
|
734
958
|
async function ensureKey(io, keyFileGiven) {
|
|
@@ -763,7 +987,7 @@ async function runCli(argv, io) {
|
|
|
763
987
|
platform: io.platform,
|
|
764
988
|
now: io.now(),
|
|
765
989
|
lang: options.lang,
|
|
766
|
-
keyFile: options["key-file"] === void 0 ||
|
|
990
|
+
keyFile: options["key-file"] === void 0 || path9.isAbsolute(options["key-file"]) ? options["key-file"] : path9.resolve(options["key-file"]),
|
|
767
991
|
display: options.display
|
|
768
992
|
});
|
|
769
993
|
for (const f of r.installed) io.out(`installed ${f}`);
|
|
@@ -784,7 +1008,7 @@ async function runCli(argv, io) {
|
|
|
784
1008
|
if (!r.removed.length) io.out("nothing to remove");
|
|
785
1009
|
for (const f of r.removed) io.out(`removed ${f}`);
|
|
786
1010
|
if (r.backup) io.out(`backup: ${r.backup}`);
|
|
787
|
-
if (r.removed.includes("context") &&
|
|
1011
|
+
if (r.removed.includes("context") && fs9.existsSync(savedKeyPath(io.home))) io.out(`kept your saved TypeSafe key at ${savedKeyPath(io.home)}`);
|
|
788
1012
|
return 0;
|
|
789
1013
|
}
|
|
790
1014
|
case "key": {
|
|
@@ -803,6 +1027,9 @@ async function runCli(argv, io) {
|
|
|
803
1027
|
case "status":
|
|
804
1028
|
for (const line of statusLines(io.home)) io.out(line);
|
|
805
1029
|
return 0;
|
|
1030
|
+
case "report":
|
|
1031
|
+
for (const line of reportLines(io.home, io.now(), options.days === void 0 ? REPORT_DAYS : Number(options.days))) io.out(line);
|
|
1032
|
+
return 0;
|
|
806
1033
|
case "help":
|
|
807
1034
|
case "--help":
|
|
808
1035
|
case "-h":
|
|
@@ -822,7 +1049,7 @@ async function runCli(argv, io) {
|
|
|
822
1049
|
// src/cli.ts
|
|
823
1050
|
runCli(process.argv.slice(2), {
|
|
824
1051
|
home: os.homedir(),
|
|
825
|
-
distDir:
|
|
1052
|
+
distDir: path10.dirname(fileURLToPath(import.meta.url)),
|
|
826
1053
|
platform: process.platform,
|
|
827
1054
|
now: () => /* @__PURE__ */ new Date(),
|
|
828
1055
|
out: (line) => console.log(line),
|