@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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import os from "os";
5
- import path9 from "path";
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 fs7 from "fs";
75
- import path8 from "path";
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: 1e5, compactMinTokens: 2e5, redRemainingPct: 20, unitDoneYes: 0.7, goalDoneYes: 0.8 },
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
- const kept = all.filter((dir) => !MOD_DIR_PATTERN.test(forwardSlashes2(dir)));
251
- if (kept.length !== all.length) {
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" && env[PLUGIN_DIRS_ENV] !== "" ? `${env[PLUGIN_DIRS_ENV]}${delimiter}` : "";
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]: `${dirs}${forwardSlashes2(modDir)}` },
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/status.ts
552
- import fs6 from "fs";
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 isRecord3(value) {
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
- if (counted.phase === "working") return `\u{1F7E2} ${k}`;
590
- if (counted.clear) return `\u{1F7E1} ${k} ${phrases.clear}`;
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 (!isRecord3(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
650
+ if (!isRecord4(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
596
651
  const j = raw.judgment;
597
- const judgment = isRecord3(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
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 isRecord4(value) {
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 = isRecord4(settings.env) ? settings.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(path7.delimiter).some((dir) => MOD_DIR_PATTERN.test(dir.replace(/\\/g, "/")));
612
- return hasDir && isRecord4(settings.pluginConfigs) && MOD_PLUGIN_NAME in settings.pluginConfigs;
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 fs6.readdirSync(stateDir(home))) {
814
+ for (const name of fs8.readdirSync(stateDir(home))) {
618
815
  if (!name.endsWith(".json")) continue;
619
- const file = path7.join(stateDir(home), name);
620
- const mtime = fs6.statSync(file).mtimeMs;
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(fs6.readFileSync(newest.file, "utf8"))) : null;
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 && !fs6.existsSync(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 !== "install") return names.length ? `options are only for install (got --${names[0]})` : null;
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 && path8.resolve(before) !== path8.resolve(file)) io.out(`The config now uses this file instead of ${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 || path8.isAbsolute(options["key-file"]) ? options["key-file"] : path8.resolve(options["key-file"]),
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") && fs7.existsSync(savedKeyPath(io.home))) io.out(`kept your saved TypeSafe key at ${savedKeyPath(io.home)}`);
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: path9.dirname(fileURLToPath(import.meta.url)),
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),