@delt/claude-jev-advisor 0.3.0 → 0.4.1

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
@@ -71,7 +71,7 @@ function terminalPrompt(terminal = PROCESS_TERMINAL) {
71
71
  }
72
72
 
73
73
  // src/cli/run.ts
74
- import fs9 from "fs";
74
+ import fs10 from "fs";
75
75
  import path9 from "path";
76
76
 
77
77
  // src/config.ts
@@ -84,6 +84,7 @@ var DEFAULT_CONFIG = {
84
84
  lang: "ko",
85
85
  keyFile: null,
86
86
  display: "mod",
87
+ shortcut: null,
87
88
  context: { enabled: true, minTokens: 25e4, compactMinTokens: 25e4, redRemainingPct: 20, unitDoneYes: 0.6, phaseDoneYes: 0.6 },
88
89
  rm: { enabled: true, jev: true, throwawayYes: 0.8, maxDirFiles: 50 }
89
90
  };
@@ -111,6 +112,7 @@ function normalizeConfig(raw) {
111
112
  lang: typeof r.lang === "string" && LANGS.includes(r.lang) ? r.lang : DEFAULT_CONFIG.lang,
112
113
  keyFile: typeof r.keyFile === "string" && r.keyFile !== "" ? r.keyFile : null,
113
114
  display: typeof r.display === "string" && DISPLAYS.includes(r.display) ? r.display : DEFAULT_CONFIG.display,
115
+ shortcut: typeof r.shortcut === "string" && r.shortcut !== "" ? r.shortcut : null,
114
116
  context: mergeSection(DEFAULT_CONFIG.context, r.version === CONFIG_VERSION ? r.context : withoutLegacyDefaults(r.context)),
115
117
  rm: mergeSection(DEFAULT_CONFIG.rm, r.rm)
116
118
  };
@@ -135,6 +137,7 @@ var claudeDir = (home) => path.join(home, ".claude");
135
137
  var dataDir = (home) => path.join(claudeDir(home), "claude-jev-advisor");
136
138
  var configPath = (home) => path.join(dataDir(home), "config.json");
137
139
  var settingsPath = (home) => path.join(claudeDir(home), "settings.json");
140
+ var keybindingsPath = (home) => path.join(claudeDir(home), "keybindings.json");
138
141
  var backupsDir = (home) => path.join(claudeDir(home), "backups");
139
142
  var stateDir = (home) => path.join(dataDir(home), "state");
140
143
  var logDir = (home) => path.join(dataDir(home), "log");
@@ -161,6 +164,11 @@ function updateConfig(home, change) {
161
164
  return next;
162
165
  }
163
166
 
167
+ // src/display/shortcut.ts
168
+ var SHORTCUT_CHORD = "ctrl+x ctrl+f";
169
+ var SHORTCUT_ACTION = "diff:back";
170
+ var SHORTCUT_CONTEXT = "DiffDialog";
171
+
164
172
  // src/features.ts
165
173
  var FEATURES = ["rm", "context"];
166
174
  var HOOK_SPECS = {
@@ -189,9 +197,187 @@ function isFeature(value) {
189
197
  // src/install.ts
190
198
  import path5 from "path";
191
199
 
192
- // src/display/settings.ts
200
+ // src/display/keybindings.ts
201
+ import fs3 from "fs";
202
+
203
+ // src/settings-file.ts
193
204
  import fs2 from "fs";
194
205
  import path3 from "path";
206
+ function readSettingsFile(file) {
207
+ let text;
208
+ try {
209
+ text = fs2.readFileSync(file, "utf8");
210
+ } catch (err) {
211
+ if (err.code === "ENOENT") return {};
212
+ throw err;
213
+ }
214
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
215
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${file} does not hold a JSON object`);
216
+ return parsed;
217
+ }
218
+ function backupName(now, fileName = "settings.json") {
219
+ const two = (n) => String(n).padStart(2, "0");
220
+ const stamp = `${now.getFullYear()}-${two(now.getMonth() + 1)}-${two(now.getDate())}-${two(now.getHours())}${two(now.getMinutes())}${two(now.getSeconds())}`;
221
+ return `${fileName}.${stamp}-before-claude-jev-advisor`;
222
+ }
223
+ function backupSettingsFile(file, dir, now) {
224
+ if (!fs2.existsSync(file)) return null;
225
+ fs2.mkdirSync(dir, { recursive: true });
226
+ const baseName = backupName(now, path3.basename(file));
227
+ let target = path3.join(dir, baseName);
228
+ let suffix = 2;
229
+ while (true) {
230
+ try {
231
+ fs2.copyFileSync(file, target, fs2.constants.COPYFILE_EXCL);
232
+ return target;
233
+ } catch (err) {
234
+ if (err.code === "EEXIST") {
235
+ target = path3.join(dir, `${baseName}-${suffix}`);
236
+ suffix++;
237
+ } else {
238
+ throw err;
239
+ }
240
+ }
241
+ }
242
+ }
243
+ function writeSettingsFile(file, settings) {
244
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
245
+ const temp = `${file}.claude-jev-advisor.tmp`;
246
+ fs2.writeFileSync(temp, `${JSON.stringify(settings, null, 2)}
247
+ `);
248
+ try {
249
+ fs2.renameSync(temp, file);
250
+ } catch (err) {
251
+ try {
252
+ fs2.unlinkSync(temp);
253
+ } catch {
254
+ }
255
+ throw err;
256
+ }
257
+ }
258
+
259
+ // src/display/keybindings.ts
260
+ var KEYBINDINGS_SCHEMA = "https://www.schemastore.org/claude-code-keybindings.json";
261
+ var KEYBINDINGS_DOCS = "https://code.claude.com/docs/en/keybindings";
262
+ function isRecord2(value) {
263
+ return typeof value === "object" && value !== null && !Array.isArray(value);
264
+ }
265
+ function isBlock(value) {
266
+ return isRecord2(value) && typeof value.context === "string" && isRecord2(value.bindings);
267
+ }
268
+ function normalizeChord(chord) {
269
+ return chord.trim().toLowerCase().split(/\s+/).map((stroke) => stroke.split("+").map((key) => key === "control" ? "ctrl" : key).join("+")).join(" ");
270
+ }
271
+ function chordKeys(block) {
272
+ const target = normalizeChord(SHORTCUT_CHORD);
273
+ return Object.keys(block.bindings).filter((key) => normalizeChord(key) === target);
274
+ }
275
+ function isOurs(block, key) {
276
+ return block.context === SHORTCUT_CONTEXT && block.bindings[key] === SHORTCUT_ACTION;
277
+ }
278
+ function parseKeybindings(raw) {
279
+ return isRecord2(raw) && Array.isArray(raw.bindings) ? raw : null;
280
+ }
281
+ function newKeybindings() {
282
+ return { $schema: KEYBINDINGS_SCHEMA, $docs: KEYBINDINGS_DOCS, bindings: [] };
283
+ }
284
+ function hasShortcut(kb) {
285
+ return kb.bindings.some((b) => isBlock(b) && chordKeys(b).some((key) => isOurs(b, key)));
286
+ }
287
+ function shortcutTakenBy(kb) {
288
+ for (const b of kb.bindings) {
289
+ if (!isBlock(b)) continue;
290
+ for (const key of chordKeys(b)) {
291
+ if (!isOurs(b, key)) return `${b.context}: ${String(b.bindings[key])}`;
292
+ }
293
+ }
294
+ return null;
295
+ }
296
+ function withShortcut(kb) {
297
+ if (hasShortcut(kb)) return kb;
298
+ const bindings = [...kb.bindings];
299
+ const at = bindings.findIndex((b) => isBlock(b) && b.context === SHORTCUT_CONTEXT);
300
+ if (at === -1) {
301
+ bindings.push({ context: SHORTCUT_CONTEXT, bindings: { [SHORTCUT_CHORD]: SHORTCUT_ACTION } });
302
+ } else {
303
+ const block = bindings[at];
304
+ bindings[at] = { ...block, bindings: { ...block.bindings, [SHORTCUT_CHORD]: SHORTCUT_ACTION } };
305
+ }
306
+ return { ...kb, bindings };
307
+ }
308
+ function withoutShortcut(kb) {
309
+ if (!hasShortcut(kb)) return kb;
310
+ const bindings = [];
311
+ for (const b of kb.bindings) {
312
+ if (!isBlock(b)) {
313
+ bindings.push(b);
314
+ continue;
315
+ }
316
+ const keys = chordKeys(b).filter((key) => isOurs(b, key));
317
+ if (!keys.length) {
318
+ bindings.push(b);
319
+ continue;
320
+ }
321
+ const rest = { ...b.bindings };
322
+ for (const key of keys) delete rest[key];
323
+ if (Object.keys(rest).length) bindings.push({ ...b, bindings: rest });
324
+ }
325
+ return { ...kb, bindings };
326
+ }
327
+ function readKeybindings(file) {
328
+ if (!fs3.existsSync(file)) return "missing";
329
+ return parseKeybindings(readSettingsFile(file));
330
+ }
331
+ function installShortcut(home, now) {
332
+ const none = (takenBy, problem) => ({ shortcut: null, takenBy, problem, backup: null });
333
+ const file = keybindingsPath(home);
334
+ try {
335
+ const read = readKeybindings(file);
336
+ if (read === null) return none(null, 'it holds no "bindings" list');
337
+ const kb = read === "missing" ? newKeybindings() : read;
338
+ const takenBy = shortcutTakenBy(kb);
339
+ if (takenBy) return none(takenBy, null);
340
+ if (hasShortcut(kb)) return { shortcut: SHORTCUT_CHORD, takenBy: null, problem: null, backup: null };
341
+ const backup = backupSettingsFile(file, backupsDir(home), now);
342
+ writeSettingsFile(file, withShortcut(kb));
343
+ return { shortcut: SHORTCUT_CHORD, takenBy: null, problem: null, backup };
344
+ } catch (err) {
345
+ return none(null, err.message);
346
+ }
347
+ }
348
+ function removeShortcut(home, now) {
349
+ const file = keybindingsPath(home);
350
+ try {
351
+ const read = readKeybindings(file);
352
+ if (read === "missing" || read === null || !hasShortcut(read)) return { removed: false, backup: null };
353
+ const backup = backupSettingsFile(file, backupsDir(home), now);
354
+ writeSettingsFile(file, withoutShortcut(read));
355
+ return { removed: true, backup };
356
+ } catch {
357
+ return { removed: false, backup: null };
358
+ }
359
+ }
360
+ function shortcutTakenInFile(home) {
361
+ try {
362
+ const read = readKeybindings(keybindingsPath(home));
363
+ return read === null || read === "missing" ? null : shortcutTakenBy(read);
364
+ } catch {
365
+ return null;
366
+ }
367
+ }
368
+ function shortcutState(home) {
369
+ try {
370
+ const read = readKeybindings(keybindingsPath(home));
371
+ if (read === null) return "unreadable";
372
+ return read !== "missing" && hasShortcut(read) ? "present" : "absent";
373
+ } catch {
374
+ return "unreadable";
375
+ }
376
+ }
377
+
378
+ // src/display/settings.ts
379
+ import fs4 from "fs";
380
+ import path4 from "path";
195
381
 
196
382
  // src/settings.ts
197
383
  var forwardSlashes = (text) => text.replace(/\\/g, "/");
@@ -212,19 +398,19 @@ function withHook(settings, entry) {
212
398
  }
213
399
  function withoutCommands(settings, pattern) {
214
400
  if (!settings.hooks) return settings;
215
- const isOurs = (h) => typeof h.command === "string" && commandMatches(h.command, pattern);
401
+ const isOurs2 = (h) => typeof h.command === "string" && commandMatches(h.command, pattern);
216
402
  const hooks = {};
217
403
  let removedAny = false;
218
404
  for (const [event, groups] of Object.entries(settings.hooks)) {
219
405
  const kept = [];
220
406
  let emptiedByUs = false;
221
407
  for (const group of groups) {
222
- if (!Array.isArray(group.hooks) || !group.hooks.some(isOurs)) {
408
+ if (!Array.isArray(group.hooks) || !group.hooks.some(isOurs2)) {
223
409
  kept.push(group);
224
410
  continue;
225
411
  }
226
412
  removedAny = true;
227
- const remaining = group.hooks.filter((h) => !isOurs(h));
413
+ const remaining = group.hooks.filter((h) => !isOurs2(h));
228
414
  if (remaining.length) kept.push({ ...group, hooks: remaining });
229
415
  else emptiedByUs = true;
230
416
  }
@@ -253,7 +439,7 @@ function findCommands(settings, pattern) {
253
439
  // src/display/settings.ts
254
440
  var PLUGIN_DIRS_ENV = "CLAUDE_CODE_PLUGIN_DIRS";
255
441
  var STATUSLINE_REFRESH_SECONDS = 3;
256
- function isRecord2(value) {
442
+ function isRecord3(value) {
257
443
  return typeof value === "object" && value !== null && !Array.isArray(value);
258
444
  }
259
445
  var forwardSlashes2 = (text) => text.replace(/\\/g, "/");
@@ -263,7 +449,7 @@ function withoutKey(settings, key) {
263
449
  }
264
450
  function withoutModDisplay(settings, delimiter) {
265
451
  let next = settings;
266
- const env = isRecord2(settings.env) ? settings.env : null;
452
+ const env = isRecord3(settings.env) ? settings.env : null;
267
453
  const dirs = env?.[PLUGIN_DIRS_ENV];
268
454
  if (env && typeof dirs === "string") {
269
455
  const all = dirs.split(delimiter);
@@ -274,7 +460,7 @@ function withoutModDisplay(settings, delimiter) {
274
460
  next = Object.keys(nextEnv).length ? { ...next, env: nextEnv } : withoutKey(next, "env");
275
461
  }
276
462
  }
277
- const configs = isRecord2(next.pluginConfigs) ? next.pluginConfigs : null;
463
+ const configs = isRecord3(next.pluginConfigs) ? next.pluginConfigs : null;
278
464
  if (configs && MOD_PLUGIN_NAME in configs) {
279
465
  const { [MOD_PLUGIN_NAME]: _ours, ...others } = configs;
280
466
  next = Object.keys(others).length ? { ...next, pluginConfigs: others } : withoutKey(next, "pluginConfigs");
@@ -283,9 +469,9 @@ function withoutModDisplay(settings, delimiter) {
283
469
  }
284
470
  function withModDisplay(settings, modDir, dataDirPath, delimiter) {
285
471
  const base = withoutModDisplay(settings, delimiter);
286
- const env = isRecord2(base.env) ? base.env : {};
472
+ const env = isRecord3(base.env) ? base.env : {};
287
473
  const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV].split(delimiter).filter(Boolean) : [];
288
- const configs = isRecord2(base.pluginConfigs) ? base.pluginConfigs : {};
474
+ const configs = isRecord3(base.pluginConfigs) ? base.pluginConfigs : {};
289
475
  return {
290
476
  ...base,
291
477
  env: { ...env, [PLUGIN_DIRS_ENV]: [...dirs, forwardSlashes2(modDir)].join(delimiter) },
@@ -293,12 +479,12 @@ function withModDisplay(settings, modDir, dataDirPath, delimiter) {
293
479
  };
294
480
  }
295
481
  function isOurStatusLine(value) {
296
- return isRecord2(value) && typeof value.command === "string" && commandMatches(value.command, ownScriptPattern(STATUSLINE_SCRIPT));
482
+ return isRecord3(value) && typeof value.command === "string" && commandMatches(value.command, ownScriptPattern(STATUSLINE_SCRIPT));
297
483
  }
298
484
  function readStatusLineBefore(home) {
299
485
  try {
300
- const value = JSON.parse(fs2.readFileSync(statusLineBeforePath(home), "utf8"));
301
- return isRecord2(value) ? value : null;
486
+ const value = JSON.parse(fs4.readFileSync(statusLineBeforePath(home), "utf8"));
487
+ return isRecord3(value) ? value : null;
302
488
  } catch {
303
489
  return null;
304
490
  }
@@ -306,17 +492,17 @@ function readStatusLineBefore(home) {
306
492
  function writeStatusLineBefore(home, value) {
307
493
  const file = statusLineBeforePath(home);
308
494
  if (!value) {
309
- fs2.rmSync(file, { force: true });
495
+ fs4.rmSync(file, { force: true });
310
496
  return;
311
497
  }
312
- fs2.mkdirSync(path3.dirname(file), { recursive: true });
313
- fs2.writeFileSync(file, `${JSON.stringify(value, null, 2)}
498
+ fs4.mkdirSync(path4.dirname(file), { recursive: true });
499
+ fs4.writeFileSync(file, `${JSON.stringify(value, null, 2)}
314
500
  `);
315
501
  }
316
502
  function applyStatusLineDisplay(settings, home, command) {
317
503
  const current = settings.statusLine;
318
504
  const ours = isOurStatusLine(current);
319
- const userLine = ours ? readStatusLineBefore(home) : isRecord2(current) ? current : null;
505
+ const userLine = ours ? readStatusLineBefore(home) : isRecord3(current) ? current : null;
320
506
  if (command !== null) {
321
507
  writeStatusLineBefore(home, userLine);
322
508
  return { ...settings, statusLine: { type: "command", command, refreshInterval: STATUSLINE_REFRESH_SECONDS } };
@@ -328,62 +514,6 @@ function forgetStatusLineBefore(home, settings) {
328
514
  if (!isOurStatusLine(settings.statusLine)) writeStatusLineBefore(home, null);
329
515
  }
330
516
 
331
- // src/settings-file.ts
332
- import fs3 from "fs";
333
- import path4 from "path";
334
- function readSettingsFile(file) {
335
- let text;
336
- try {
337
- text = fs3.readFileSync(file, "utf8");
338
- } catch (err) {
339
- if (err.code === "ENOENT") return {};
340
- throw err;
341
- }
342
- const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
343
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`${file} does not hold a JSON object`);
344
- return parsed;
345
- }
346
- function backupName(now) {
347
- const two = (n) => String(n).padStart(2, "0");
348
- const stamp = `${now.getFullYear()}-${two(now.getMonth() + 1)}-${two(now.getDate())}-${two(now.getHours())}${two(now.getMinutes())}${two(now.getSeconds())}`;
349
- return `settings.json.${stamp}-before-claude-jev-advisor`;
350
- }
351
- function backupSettingsFile(file, dir, now) {
352
- if (!fs3.existsSync(file)) return null;
353
- fs3.mkdirSync(dir, { recursive: true });
354
- const baseName = backupName(now);
355
- let target = path4.join(dir, baseName);
356
- let suffix = 2;
357
- while (true) {
358
- try {
359
- fs3.copyFileSync(file, target, fs3.constants.COPYFILE_EXCL);
360
- return target;
361
- } catch (err) {
362
- if (err.code === "EEXIST") {
363
- target = path4.join(dir, `${baseName}-${suffix}`);
364
- suffix++;
365
- } else {
366
- throw err;
367
- }
368
- }
369
- }
370
- }
371
- function writeSettingsFile(file, settings) {
372
- fs3.mkdirSync(path4.dirname(file), { recursive: true });
373
- const temp = `${file}.claude-jev-advisor.tmp`;
374
- fs3.writeFileSync(temp, `${JSON.stringify(settings, null, 2)}
375
- `);
376
- try {
377
- fs3.renameSync(temp, file);
378
- } catch (err) {
379
- try {
380
- fs3.unlinkSync(temp);
381
- } catch {
382
- }
383
- throw err;
384
- }
385
- }
386
-
387
517
  // src/install.ts
388
518
  function withoutFeature(settings, feature) {
389
519
  return HOOK_SPECS[feature].reduce((s, spec) => withoutCommands(s, ownScriptPattern(spec.script)), settings);
@@ -436,15 +566,20 @@ function install(opts) {
436
566
  if (display !== null) next = withDisplay(next, { home: opts.home, distDir: opts.distDir, display, delimiter });
437
567
  const backup = same(before, next) ? null : save(opts.home, next, opts.now);
438
568
  forgetStatusLineBefore(opts.home, next);
569
+ let shortcut = null;
570
+ let shortcutRemoved = null;
571
+ if (display === "mod") shortcut = installShortcut(opts.home, opts.now);
572
+ else if (display !== null) shortcutRemoved = removeShortcut(opts.home, opts.now);
439
573
  updateConfig(opts.home, (c) => ({
440
574
  ...c,
441
575
  lang: opts.lang ?? c.lang,
442
576
  keyFile: opts.keyFile ?? c.keyFile,
443
577
  display: opts.display ?? c.display,
578
+ shortcut: shortcut ? shortcut.shortcut : display !== null ? null : c.shortcut,
444
579
  rm: installed.includes("rm") ? { ...c.rm, enabled: true } : c.rm,
445
580
  context: installed.includes("context") ? { ...c.context, enabled: true } : c.context
446
581
  }));
447
- return { settingsFile: file, backup, installed, skipped, replacedLegacyRmGuard, display };
582
+ return { settingsFile: file, backup, installed, skipped, replacedLegacyRmGuard, display, shortcut, shortcutRemoved };
448
583
  }
449
584
  function uninstall(opts) {
450
585
  const delimiter = opts.delimiter ?? path5.delimiter;
@@ -460,7 +595,12 @@ function uninstall(opts) {
460
595
  }
461
596
  const backup = removed.length ? save(opts.home, next, opts.now) : null;
462
597
  forgetStatusLineBefore(opts.home, next);
463
- return { settingsFile: file, backup, removed };
598
+ let shortcut = null;
599
+ if (opts.features.includes("context")) {
600
+ shortcut = removeShortcut(opts.home, opts.now);
601
+ if (readConfig(opts.home).shortcut !== null) updateConfig(opts.home, (c) => ({ ...c, shortcut: null }));
602
+ }
603
+ return { settingsFile: file, backup, removed, shortcut };
464
604
  }
465
605
  function setEnabled(home, features, enabled) {
466
606
  updateConfig(home, (c) => ({
@@ -471,7 +611,7 @@ function setEnabled(home, features, enabled) {
471
611
  }
472
612
 
473
613
  // src/jev.ts
474
- import fs4 from "fs";
614
+ import fs5 from "fs";
475
615
  var JEV_URL = "https://api.typesafe.ai/v1/systemone";
476
616
  var JEV_TIMEOUT_MS = 8e3;
477
617
  var KEY_PREFIX = "TYPESAFE_API_KEY=";
@@ -481,7 +621,7 @@ function readJevKey(env, keyFile) {
481
621
  if (!keyFile) return null;
482
622
  let text;
483
623
  try {
484
- text = fs4.readFileSync(keyFile, "utf8");
624
+ text = fs5.readFileSync(keyFile, "utf8");
485
625
  } catch {
486
626
  return null;
487
627
  }
@@ -534,7 +674,7 @@ async function callJev(request, opts) {
534
674
  }
535
675
 
536
676
  // src/key.ts
537
- import fs5 from "fs";
677
+ import fs6 from "fs";
538
678
  import path6 from "path";
539
679
  var CHECK_REQUEST = {
540
680
  state: { text: "hello" },
@@ -548,10 +688,10 @@ async function checkJevKey(key, fetchFn) {
548
688
  }
549
689
  function saveJevKey(home, key) {
550
690
  const file = savedKeyPath(home);
551
- fs5.mkdirSync(path6.dirname(file), { recursive: true });
552
- fs5.writeFileSync(file, `TYPESAFE_API_KEY=${key}
691
+ fs6.mkdirSync(path6.dirname(file), { recursive: true });
692
+ fs6.writeFileSync(file, `TYPESAFE_API_KEY=${key}
553
693
  `, { mode: 384 });
554
- fs5.chmodSync(file, 384);
694
+ fs6.chmodSync(file, 384);
555
695
  return file;
556
696
  }
557
697
  async function promptForKey(prompt, check) {
@@ -569,15 +709,15 @@ async function promptForKey(prompt, check) {
569
709
  }
570
710
 
571
711
  // src/report.ts
572
- import fs7 from "fs";
712
+ import fs8 from "fs";
573
713
  import path7 from "path";
574
714
 
575
715
  // src/context/transcript.ts
576
- import fs6 from "fs";
716
+ import fs7 from "fs";
577
717
  var TAIL_START_BYTES = 1024 * 1024;
578
718
  var TAIL_MAX_BYTES = 8 * 1024 * 1024;
579
719
  var REQUEST_ORIGINS = /* @__PURE__ */ new Set(["human", "channel", "peer"]);
580
- function isRecord3(value) {
720
+ function isRecord4(value) {
581
721
  return typeof value === "object" && value !== null && !Array.isArray(value);
582
722
  }
583
723
  function parseLines(text) {
@@ -586,7 +726,7 @@ function parseLines(text) {
586
726
  if (!line.trim()) continue;
587
727
  try {
588
728
  const value = JSON.parse(line);
589
- if (isRecord3(value)) entries.push(value);
729
+ if (isRecord4(value)) entries.push(value);
590
730
  } catch {
591
731
  }
592
732
  }
@@ -595,29 +735,31 @@ function parseLines(text) {
595
735
  function messageText(content) {
596
736
  if (typeof content === "string") return content;
597
737
  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");
738
+ return content.filter((block) => isRecord4(block) && block.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n");
599
739
  }
600
740
  function isRequest(entry) {
601
- return entry.type === "user" && entry.isSidechain !== true && isRecord3(entry.origin) && REQUEST_ORIGINS.has(String(entry.origin.kind));
741
+ return entry.type === "user" && entry.isSidechain !== true && isRecord4(entry.origin) && REQUEST_ORIGINS.has(String(entry.origin.kind));
602
742
  }
603
743
  function requestText(entry) {
604
- return messageText(isRecord3(entry.message) ? entry.message.content : void 0).trim();
744
+ return messageText(isRecord4(entry.message) ? entry.message.content : void 0).trim();
605
745
  }
606
746
 
607
747
  // src/display/line.ts
608
- var PHRASES = {
748
+ var QUESTIONS = {
609
749
  ko: {
610
- clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /clear",
611
- compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact",
612
- compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694? /compact"
750
+ clear: "\uC0C8\uB86D\uAC8C \uC2DC\uC791\uD558\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
751
+ compact: "\uC9C0\uAE08\uAE4C\uC9C0 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?",
752
+ compactLater: "\uC791\uC5C5\uC774 \uB05D\uB098\uBA74 \uC815\uB9AC\uD558\uACE0 \uC774\uC5B4\uAC00\uB294 \uAC74 \uC5B4\uB5A0\uC138\uC694?"
613
753
  },
614
754
  en: {
615
- clear: "Start fresh? /clear",
616
- compact: "Wrap up what you have and continue? /compact",
617
- compactLater: "When this work is done, wrap up and continue? /compact"
755
+ clear: "Start fresh?",
756
+ compact: "Wrap up what you have and continue?",
757
+ compactLater: "When this work is done, wrap up and continue?"
618
758
  }
619
759
  };
620
- function isRecord4(value) {
760
+ var COMMANDS = { clear: "/clear", compact: "/compact", compactLater: "/compact" };
761
+ var phrase = (lang, advice) => `${QUESTIONS[lang][advice]} ${COMMANDS[advice]}`;
762
+ function isRecord5(value) {
621
763
  return typeof value === "object" && value !== null && !Array.isArray(value);
622
764
  }
623
765
  function formatSize(tokens) {
@@ -631,32 +773,41 @@ function adviceKind(size, judgment, config) {
631
773
  if (judgment.clear) return "clear";
632
774
  return size >= config.context.compactMinTokens ? "compact" : null;
633
775
  }
634
- function adviceLine({ size, threshold, judgment, config }) {
776
+ function increaseSuffix(increase) {
777
+ if (increase === null || increase === void 0) return "";
778
+ const k = Math.round(increase / 1e3);
779
+ return k > 0 ? ` +${k}k` : "";
780
+ }
781
+ function redRemaining(size, threshold, config) {
782
+ if (threshold === null || threshold <= 0 || size < threshold * (1 - config.context.redRemainingPct / 100)) return null;
783
+ return remainingPct(size, threshold);
784
+ }
785
+ function adviceLine({ size, threshold, judgment, config, increase }) {
635
786
  if (size === null) return "";
636
- const k = formatSize(size);
637
- const phrases = PHRASES[config.lang];
787
+ const k = `${formatSize(size)}${increaseSuffix(increase)}`;
638
788
  const counted = size >= config.context.minTokens ? judgment : null;
639
- if (threshold !== null && threshold > 0 && size >= threshold * (1 - config.context.redRemainingPct / 100)) {
640
- const head = `\u{1F534} ${k} ${remainingPct(size, threshold)}%`;
789
+ const red = redRemaining(size, threshold, config);
790
+ if (red !== null) {
791
+ const head = `\u{1F534} ${k} ${red}%`;
641
792
  if (!counted) return head;
642
- if (counted.phase === "working") return `${head} \xB7 ${phrases.compactLater}`;
643
- return `${head} \xB7 ${counted.clear ? phrases.clear : phrases.compact}`;
793
+ if (counted.phase === "working") return `${head} \xB7 ${phrase(config.lang, "compactLater")}`;
794
+ return `${head} \xB7 ${phrase(config.lang, counted.clear ? "clear" : "compact")}`;
644
795
  }
645
796
  if (!counted) return k;
646
797
  const kind = adviceKind(size, counted, config);
647
- return kind ? `\u{1F7E1} ${k} ${phrases[kind]}` : `\u{1F7E2} ${k}`;
798
+ return kind ? `\u{1F7E1} ${k} ${phrase(config.lang, kind)}` : `\u{1F7E2} ${k}`;
648
799
  }
649
800
  function parseState(raw) {
650
- if (!isRecord4(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
801
+ if (!isRecord5(raw) || typeof raw.sessionId !== "string" || typeof raw.at !== "number") return null;
651
802
  const j = raw.judgment;
652
- const judgment = isRecord4(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
803
+ const judgment = isRecord5(j) && (j.phase === "working" || j.phase === "unit_done") && typeof j.clear === "boolean" ? { phase: j.phase, clear: j.clear } : null;
653
804
  return { sessionId: raw.sessionId, at: raw.at, size: typeof raw.size === "number" ? raw.size : null, judgment };
654
805
  }
655
806
 
656
807
  // src/report.ts
657
808
  var FOLLOW_REQUESTS = 3;
658
809
  var DAY_MS = 24 * 60 * 60 * 1e3;
659
- function isRecord5(value) {
810
+ function isRecord6(value) {
660
811
  return typeof value === "object" && value !== null && !Array.isArray(value);
661
812
  }
662
813
  function monthFiles(home, since, now) {
@@ -671,7 +822,7 @@ function readLogRecords(home, since, now) {
671
822
  for (const file of monthFiles(home, since, now)) {
672
823
  let text;
673
824
  try {
674
- text = fs7.readFileSync(file, "utf8");
825
+ text = fs8.readFileSync(file, "utf8");
675
826
  } catch {
676
827
  continue;
677
828
  }
@@ -693,7 +844,7 @@ function outcomeAfter(entries, at, endedByClear) {
693
844
  const time = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : NaN;
694
845
  if (!(time > at)) continue;
695
846
  if (entry.type === "system" && entry.subtype === "compact_boundary") {
696
- const trigger = isRecord5(entry.compactMetadata) ? entry.compactMetadata.trigger : void 0;
847
+ const trigger = isRecord6(entry.compactMetadata) ? entry.compactMetadata.trigger : void 0;
697
848
  return { kind: trigger === "auto" ? "auto_compact" : "compact" };
698
849
  }
699
850
  if (isRequest(entry) && requestText(entry)) {
@@ -712,7 +863,7 @@ function adviceRows(home, now, days) {
712
863
  if (typeof file !== "string") return null;
713
864
  if (!transcripts.has(file)) {
714
865
  try {
715
- transcripts.set(file, parseLines(fs7.readFileSync(file, "utf8")));
866
+ transcripts.set(file, parseLines(fs8.readFileSync(file, "utf8")));
716
867
  } catch {
717
868
  transcripts.set(file, null);
718
869
  }
@@ -726,8 +877,8 @@ function adviceRows(home, now, days) {
726
877
  if (!advice) continue;
727
878
  const at = new Date(record.at);
728
879
  const entries = entriesOf(record.transcriptPath);
729
- const jev = isRecord5(record.jev) ? record.jev : {};
730
- const state = isRecord5(jev.state) ? jev.state : {};
880
+ const jev = isRecord6(record.jev) ? record.jev : {};
881
+ const state = isRecord6(jev.state) ? jev.state : {};
731
882
  const requests = Array.isArray(state.recent_requests) ? state.recent_requests : [];
732
883
  const last = requests[requests.length - 1];
733
884
  rows.push({
@@ -735,7 +886,7 @@ function adviceRows(home, now, days) {
735
886
  sessionId: record.sessionId,
736
887
  size: record.size,
737
888
  advice,
738
- answers: isRecord5(jev.answers) ? jev.answers : null,
889
+ answers: isRecord6(jev.answers) ? jev.answers : null,
739
890
  request: typeof last === "string" ? last : "",
740
891
  outcome: entries ? outcomeAfter(entries, at.getTime(), cleared.has(record.sessionId)) : { kind: "unknown" }
741
892
  });
@@ -769,7 +920,7 @@ function rmLines(home, now, days) {
769
920
  const passed = records.filter((r) => r.decision === "pass");
770
921
  const lines = [`rm deletions Jev let through: ${passed.length} (asked anyway after a Jev check: ${records.length - passed.length})`];
771
922
  for (const record of passed) {
772
- const targets = Array.isArray(record.targets) ? record.targets.filter(isRecord5) : [];
923
+ const targets = Array.isArray(record.targets) ? record.targets.filter(isRecord6) : [];
773
924
  const listed = targets.map((t) => `${String(t.shown)} (${typeof t.p === "number" ? t.p.toFixed(2) : "?"})`).join(", ");
774
925
  lines.push(` ${localTime(new Date(record.at))} ${String(record.sessionId ?? "").slice(0, 8)} ${listed}`);
775
926
  }
@@ -794,34 +945,43 @@ function reportLines(home, now, days) {
794
945
  }
795
946
 
796
947
  // src/status.ts
797
- import fs8 from "fs";
948
+ import fs9 from "fs";
798
949
  import path8 from "path";
799
950
  var SCRIPT_IN_COMMAND = /^node "([^"]+)"/;
800
- function isRecord6(value) {
951
+ function isRecord7(value) {
801
952
  return typeof value === "object" && value !== null && !Array.isArray(value);
802
953
  }
803
954
  function displaySetUp(settings, config) {
804
955
  if (config.display === "message") return true;
805
956
  if (config.display === "statusline") return isOurStatusLine(settings.statusLine);
806
- const env = isRecord6(settings.env) ? settings.env : {};
957
+ const env = isRecord7(settings.env) ? settings.env : {};
807
958
  const dirs = typeof env[PLUGIN_DIRS_ENV] === "string" ? env[PLUGIN_DIRS_ENV] : "";
808
959
  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;
960
+ return hasDir && isRecord7(settings.pluginConfigs) && MOD_PLUGIN_NAME in settings.pluginConfigs;
810
961
  }
811
962
  function newestState(home) {
812
963
  let newest = null;
813
964
  try {
814
- for (const name of fs8.readdirSync(stateDir(home))) {
965
+ for (const name of fs9.readdirSync(stateDir(home))) {
815
966
  if (!name.endsWith(".json")) continue;
816
967
  const file = path8.join(stateDir(home), name);
817
- const mtime = fs8.statSync(file).mtimeMs;
968
+ const mtime = fs9.statSync(file).mtimeMs;
818
969
  if (!newest || mtime > newest.mtime) newest = { file, mtime };
819
970
  }
820
- return newest ? parseState(JSON.parse(fs8.readFileSync(newest.file, "utf8"))) : null;
971
+ return newest ? parseState(JSON.parse(fs9.readFileSync(newest.file, "utf8"))) : null;
821
972
  } catch {
822
973
  return null;
823
974
  }
824
975
  }
976
+ function shortcutLine(home, config) {
977
+ const takenBy = shortcutTakenInFile(home);
978
+ if (!config.shortcut) return takenBy ? ` shortcut: none - ${SHORTCUT_CHORD} is already bound (${takenBy}); press the button with a click, or ctrl+x tab then Enter` : ` shortcut: none - run "claude-jev-advisor install context" to bind ${SHORTCUT_CHORD}`;
979
+ if (takenBy) return ` shortcut: ${config.shortcut} (also bound by ${takenBy}, which wins - run "claude-jev-advisor install context")`;
980
+ const state = shortcutState(home);
981
+ if (state === "present") return ` shortcut: ${config.shortcut}`;
982
+ const why = state === "unreadable" ? "keybindings.json cannot be read" : "missing from keybindings.json";
983
+ return ` shortcut: ${config.shortcut} (${why} - run "claude-jev-advisor install context")`;
984
+ }
825
985
  function contextLines(home, settings, config, env) {
826
986
  const c = config.context;
827
987
  const lines = [
@@ -830,6 +990,7 @@ function contextLines(home, settings, config, env) {
830
990
  ` thresholds: judge from ${formatSize(c.minTokens)}, /compact from ${formatSize(c.compactMinTokens)}, red at ${c.redRemainingPct}% left`,
831
991
  ` Jev: work done from ${c.unitDoneYes}, stage done (/clear) from ${c.phaseDoneYes}`
832
992
  ];
993
+ if (config.display === "mod") lines.splice(1, 0, shortcutLine(home, config));
833
994
  const last = newestState(home);
834
995
  if (last) {
835
996
  const line = adviceLine({ size: last.size, threshold: null, judgment: last.judgment, config }) || "(no size)";
@@ -869,7 +1030,7 @@ function statusLines(home, env = process.env) {
869
1030
  lines.push(`${feature}: installed, ${state}`);
870
1031
  for (const c of commands) {
871
1032
  const script = SCRIPT_IN_COMMAND.exec(c.command)?.[1];
872
- if (script && !fs8.existsSync(script)) {
1033
+ if (script && !fs9.existsSync(script)) {
873
1034
  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}"`);
874
1035
  }
875
1036
  }
@@ -968,6 +1129,11 @@ async function ensureKey(io, keyFileGiven) {
968
1129
  }
969
1130
  await askAndSaveKey(io, io.prompt);
970
1131
  }
1132
+ function shortcutLine2(s) {
1133
+ if (s.shortcut) return `shortcut: ${s.shortcut} presses the advice button above the prompt (~/.claude/keybindings.json)`;
1134
+ const why = s.takenBy ? `${SHORTCUT_CHORD} is already bound (${s.takenBy})` : `keybindings.json: ${s.problem}`;
1135
+ return `shortcut: none - ${why}; press the button with a click, or ctrl+x tab then Enter`;
1136
+ }
971
1137
  async function runCli(argv, io) {
972
1138
  const parsed = parse(argv);
973
1139
  const problem = typeof parsed === "string" ? parsed : checkOptions(parsed.command, parsed.options);
@@ -995,9 +1161,15 @@ async function runCli(argv, io) {
995
1161
  if (r.replacedLegacyRmGuard) io.out("replaced the legacy rm-guard hook (its files under ~/.claude/hooks/rm-guard were left in place)");
996
1162
  if (r.display) io.out(`display: ${r.display}`);
997
1163
  if (r.backup) io.out(`backup: ${r.backup}`);
1164
+ if (r.shortcut) io.out(shortcutLine2(r.shortcut));
1165
+ if (r.shortcut?.backup) io.out(`backup: ${r.shortcut.backup}`);
1166
+ if (r.shortcutRemoved?.removed) {
1167
+ io.out(`removed the ${SHORTCUT_CHORD} shortcut from keybindings.json`);
1168
+ if (r.shortcutRemoved.backup) io.out(`backup: ${r.shortcutRemoved.backup}`);
1169
+ }
998
1170
  if (r.installed.length) {
999
1171
  io.out(
1000
- r.display === "mod" ? "Hooks apply right away, also in open Claude Code sessions; the bottom-row display starts with the next new session." : "Hooks apply right away, also in open Claude Code sessions."
1172
+ r.display === "mod" ? "Hooks apply right away, also in open Claude Code sessions. The bottom-row display and the advice button start with the next new session; sessions that already show the bottom-row display pick up an upgrade by themselves." : "Hooks apply right away, also in open Claude Code sessions."
1001
1173
  );
1002
1174
  }
1003
1175
  if (r.installed.includes("context")) await ensureKey(io, options["key-file"] !== void 0);
@@ -1008,7 +1180,9 @@ async function runCli(argv, io) {
1008
1180
  if (!r.removed.length) io.out("nothing to remove");
1009
1181
  for (const f of r.removed) io.out(`removed ${f}`);
1010
1182
  if (r.backup) io.out(`backup: ${r.backup}`);
1011
- if (r.removed.includes("context") && fs9.existsSync(savedKeyPath(io.home))) io.out(`kept your saved TypeSafe key at ${savedKeyPath(io.home)}`);
1183
+ if (r.shortcut?.removed) io.out(`removed the ${SHORTCUT_CHORD} shortcut from keybindings.json`);
1184
+ if (r.shortcut?.backup) io.out(`backup: ${r.shortcut.backup}`);
1185
+ if (r.removed.includes("context") && fs10.existsSync(savedKeyPath(io.home))) io.out(`kept your saved TypeSafe key at ${savedKeyPath(io.home)}`);
1012
1186
  return 0;
1013
1187
  }
1014
1188
  case "key": {