claude-doom-statusbar 0.3.1 → 0.4.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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-doom-statusbar",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "DOOM-inspired status bar for the Claude Code CLI — a mugshot that tracks session health, plus usage, model, project, system, and a live subagent list.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "scripts": {
16
16
  "statusline": "node src/statusline.js",
17
17
  "hook": "node src/hook.js",
18
- "test": "node test/installer.test.mjs && node test/smoke.test.mjs && node test/hook-tasks.test.mjs && node test/statusline-tasks.test.mjs && node test/render-scroll.test.mjs && node test/e2e-tasks.test.mjs",
18
+ "test": "node test/installer.test.mjs && node test/smoke.test.mjs && node test/hook-tasks.test.mjs && node test/statusline-tasks.test.mjs && node test/render-scroll.test.mjs && node test/statusline-savings.test.mjs && node test/e2e-tasks.test.mjs",
19
19
  "preversion": "npm test",
20
20
  "postversion": "git push --follow-tags",
21
21
  "prepublishOnly": "npm test"
@@ -17,6 +17,14 @@ metric = [
17
17
  { id = "ratelimit.7d", render = "bar", icon = "📅", color = "threshold" },
18
18
  ]
19
19
 
20
+ [[segment]]
21
+ type = "box"
22
+ title = "SAVE"
23
+ metric = [
24
+ { id = "save.leanctx", render = "text", icon = "ðŸŠķ" },
25
+ { id = "save.lingua", render = "text", icon = "📜" },
26
+ ]
27
+
20
28
  [[segment]]
21
29
  type = "mugshot"
22
30
 
package/presets/full.toml CHANGED
@@ -30,6 +30,14 @@ metric = [
30
30
  { id = "cost.total", render = "number", icon = "💰" },
31
31
  ]
32
32
 
33
+ [[segment]]
34
+ type = "box"
35
+ title = "SAVE"
36
+ metric = [
37
+ { id = "save.leanctx", render = "text", icon = "ðŸŠķ" },
38
+ { id = "save.lingua", render = "text", icon = "📜" },
39
+ ]
40
+
33
41
  [[segment]]
34
42
  type = "box"
35
43
  title = "PROJECT"
package/src/render.js CHANGED
@@ -47,6 +47,7 @@ export const SAMPLE = {
47
47
  "act.geiger": [0, .25, .5, 1, .75, 1, .5, .6, .3, .1, .4, 1, .8, .4],
48
48
  "act.tasks": "2/5", "act.errors": "0", "sys.ram": 47, "sys.cpu": "12%",
49
49
  "sys.disk": 63, "sys.clock": "14:23",
50
+ "save.leanctx": "8.3k 63%", "save.lingua": "1.2k 1.3x",
50
51
  };
51
52
 
52
53
  let VALUES = SAMPLE;
package/src/statusline.js CHANGED
@@ -45,6 +45,10 @@ function git(cwd, ...args) {
45
45
  // so an oversized repo or branch name can't blow up the PROJECT box width.
46
46
  const clip = (s, n) => ([...String(s)].length > n ? [...String(s)].slice(0, n - 1).join("") + "â€Ķ" : String(s));
47
47
 
48
+ // Human-readable token count: 8263 -> "8.3k", 1200000 -> "1.2M", 512 -> "512".
49
+ // Lowercase k/M — the savings rows read softer than model.window's uppercase K/M.
50
+ const k = (n) => (n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : `${n}`);
51
+
48
52
  function _dur(secsF) {
49
53
  let secs = Math.max(0, Math.trunc(secsF));
50
54
  const d = Math.floor(secs / 86400); secs %= 86400;
@@ -228,6 +232,60 @@ function sysValues(cwd) {
228
232
  return v;
229
233
  }
230
234
 
235
+ // Token-savings rows read from the small JSON files context-optimization tools already
236
+ // persist. No plugin patching, no binary spawn — just a cheap read each refresh. Paths are
237
+ // env-overridable (DOOMBAR_*) so tests can point at fixtures, mirroring statePath/MUGSHOT_STATE.
238
+ const leanCtxPath = () => process.env.DOOMBAR_LEANCTX || path.join(os.homedir(), ".lean-ctx", "mcp-live.json");
239
+ const llmlinguaPath = () => process.env.DOOMBAR_LLMLINGUA || path.join(os.homedir(), ".llmlingua-stats.json");
240
+
241
+ // Defensive read: missing file or malformed JSON -> null (the row simply never appears).
242
+ function readJson(p) {
243
+ try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; }
244
+ }
245
+
246
+ // One entry per savings source; extract returns the display string or null (omit the row).
247
+ // Adding a source later is one entry here plus one preset line — not an adapter framework.
248
+ const SAVINGS_SOURCES = [
249
+ {
250
+ key: "save.leanctx",
251
+ path: leanCtxPath,
252
+ extract: (d) => {
253
+ if (!(d.tokens_saved > 0)) return null;
254
+ // compression_rate is a 0-100 percentage (verified against historical data).
255
+ // Round it — a fractional value would render "63.45%" and shift the box width.
256
+ return typeof d.compression_rate === "number"
257
+ ? `${k(d.tokens_saved)} ${Math.round(d.compression_rate)}%`
258
+ : k(d.tokens_saved);
259
+ },
260
+ },
261
+ {
262
+ key: "save.lingua",
263
+ path: llmlinguaPath,
264
+ extract: (d) => {
265
+ // Prefer the nested session schema (smart-read). The flat lifetime-only shape
266
+ // (llmlingua_logged.py: tokens_saved_total, no session) is absent for the session view.
267
+ const s = d.session;
268
+ if (!s || !(s.tokens_saved > 0)) return null;
269
+ // Round/clamp the secondary figure so a many-decimal value can't shift box width.
270
+ if (s.last_saved_pct != null) return `${k(s.tokens_saved)} ${Math.round(s.last_saved_pct)}%`;
271
+ // No original-token count in the session block, so a percent isn't derivable; show the ratio.
272
+ if (s.last_ratio != null) return `${k(s.tokens_saved)} ${Number(s.last_ratio).toFixed(1)}x`;
273
+ return k(s.tokens_saved);
274
+ },
275
+ },
276
+ ];
277
+
278
+ export function statsValues() {
279
+ const v = {};
280
+ for (const src of SAVINGS_SOURCES) {
281
+ const data = readJson(src.path());
282
+ if (!data) continue;
283
+ const out = src.extract(data);
284
+ if (out) v[src.key] = out; // omit on null/zero -> render.js available() drops the row
285
+ }
286
+ return v;
287
+ }
288
+
231
289
  const PERM = { plan: "📋 plan", auto: "âĐ auto", acceptEdits: "âĐ auto", bypassPermissions: "âĐ bypass" };
232
290
  const OK_RGB = [96, 200, 104]; // matches render.js OK (done, green)
233
291
  const CRIT_RGB = [224, 84, 64]; // matches render.js CRIT (deleted, red)
@@ -287,7 +345,7 @@ function main() {
287
345
  const now = Date.now() / 1000;
288
346
  const st = readState(data);
289
347
  const cwd = data.cwd || (data.workspace || {}).current_dir;
290
- const values = { ...buildValues(data), ...activityValues(st, now), ...sysValues(cwd) };
348
+ const values = { ...buildValues(data), ...activityValues(st, now), ...sysValues(cwd), ...statsValues() };
291
349
  const [advModel, advTs] = advisorInfo(data.transcript_path || "");
292
350
  if (advModel) values["advisor.model"] = advModel;
293
351
  const god_until = godFlash(data, advTs, now);