claude-doom-statusbar 0.3.1 â 0.5.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/assets/images/hud.png +0 -0
- package/package.json +2 -2
- package/presets/default.toml +8 -0
- package/presets/full.toml +8 -0
- package/src/render.js +1 -0
- package/src/statusline.js +109 -2
package/assets/images/hud.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-doom-statusbar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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"
|
package/presets/default.toml
CHANGED
|
@@ -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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Config: $DOOMBAR_PRESET (default presets/default.toml) State: $MUGSHOT_STATE
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
readFileSync, writeFileSync, openSync, fstatSync, readSync, closeSync, statfsSync,
|
|
13
|
+
readFileSync, writeFileSync, openSync, fstatSync, readSync, closeSync, statfsSync, statSync,
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import os from "node:os";
|
|
16
16
|
import path from "node:path";
|
|
@@ -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,109 @@ function sysValues(cwd) {
|
|
|
228
232
|
return v;
|
|
229
233
|
}
|
|
230
234
|
|
|
235
|
+
// --- Token-savings rows -----------------------------------------------------
|
|
236
|
+
// Read from what context-optimization tools already persist on disk. No plugin
|
|
237
|
+
// patching, no binary spawn. Paths are env-overridable (DOOMBAR_*) for tests.
|
|
238
|
+
const eventsPath = () => process.env.DOOMBAR_EVENTS || path.join(os.homedir(), ".lean-ctx", "events.jsonl");
|
|
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
|
+
// Windows FS is case-insensitive, POSIX is not â only fold case on win32 to avoid
|
|
247
|
+
// false path matches (e.g. /home/User vs /home/user) on the platforms the npm package also runs on.
|
|
248
|
+
const normPath = (p) => {
|
|
249
|
+
const s = String(p).replace(/\\/g, "/");
|
|
250
|
+
return process.platform === "win32" ? s.toLowerCase() : s;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// "8.3k 63%" â saved + a per-session compression rate derived from accumulated totals.
|
|
254
|
+
function fmtSaved(st) {
|
|
255
|
+
return st.original > 0 ? `${k(st.saved)} ${Math.round(100 * st.saved / st.original)}%` : k(st.saved);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const savingsStatePath = (sid) =>
|
|
259
|
+
process.env.DOOMBAR_SAVINGS_STATE || path.join(TMP, `savings_${sid}.json`);
|
|
260
|
+
|
|
261
|
+
// Per-session lean-ctx savings. lean-ctx's mcp-live.json is a single global file clobbered
|
|
262
|
+
// by every concurrent session, so it can't be per-session. events.jsonl is append-only and
|
|
263
|
+
// each ToolCall carries the file path it compressed â so we sum tokens_saved over NEW events
|
|
264
|
+
// (tracked by byte offset) whose path is under the current cwd, accumulated across refreshes
|
|
265
|
+
// in a per-session state file keyed by session_id. The accumulator follows cwd changes (it
|
|
266
|
+
// keeps adding wherever you currently work) and stays cheap â it reads only the bytes appended
|
|
267
|
+
// since the last refresh, never the whole log. Residual: two sessions concurrently in the SAME
|
|
268
|
+
// project share events and both count them â unsplittable from disk, accepted.
|
|
269
|
+
function leanCtxSavings(cwd, sid) {
|
|
270
|
+
if (!cwd) return null;
|
|
271
|
+
const cwdN = normPath(cwd).replace(/\/+$/, "") + "/"; // match on a path boundary, not a prefix
|
|
272
|
+
const sp = savingsStatePath(sid);
|
|
273
|
+
let st = { offset: null, saved: 0, original: 0 };
|
|
274
|
+
try { st = { ...st, ...JSON.parse(readFileSync(sp, "utf8")) }; } catch { /* fresh session */ }
|
|
275
|
+
|
|
276
|
+
let size = -1;
|
|
277
|
+
try { size = statSync(eventsPath()).size; } catch { /* no log */ }
|
|
278
|
+
if (size < 0) return st.saved > 0 ? fmtSaved(st) : null; // log gone -> keep prior total
|
|
279
|
+
|
|
280
|
+
if (st.offset === null) st.offset = size; // first sight of this session: count from now on
|
|
281
|
+
// Log shrank -> lean-ctx rotated it (old events go to archives/, the restarted log holds only
|
|
282
|
+
// NEW events). Keep the running total and re-read from 0: prior total + new events = correct.
|
|
283
|
+
// (An in-place truncate-and-rewrite retaining old content would double-count, but an append-only
|
|
284
|
+
// log doesn't do that.)
|
|
285
|
+
if (st.offset > size) st.offset = 0;
|
|
286
|
+
|
|
287
|
+
if (size > st.offset) {
|
|
288
|
+
let chunk = "";
|
|
289
|
+
try {
|
|
290
|
+
const fd = openSync(eventsPath(), "r");
|
|
291
|
+
const buf = Buffer.alloc(size - st.offset);
|
|
292
|
+
readSync(fd, buf, 0, buf.length, st.offset);
|
|
293
|
+
closeSync(fd);
|
|
294
|
+
chunk = buf.toString("utf8");
|
|
295
|
+
} catch { chunk = ""; }
|
|
296
|
+
const lastNl = chunk.lastIndexOf("\n"); // consume complete lines only; keep any partial tail
|
|
297
|
+
if (lastNl >= 0) {
|
|
298
|
+
for (const ln of chunk.slice(0, lastNl).split("\n")) {
|
|
299
|
+
if (!ln) continue;
|
|
300
|
+
let o; try { o = JSON.parse(ln); } catch { continue; }
|
|
301
|
+
const ev = o.kind;
|
|
302
|
+
if (!ev || ev.type !== "ToolCall" || !ev.path) continue;
|
|
303
|
+
if (!normPath(ev.path).startsWith(cwdN)) continue; // cwdN ends in "/" -> boundary-safe
|
|
304
|
+
st.saved += ev.tokens_saved || 0;
|
|
305
|
+
st.original += ev.tokens_original || 0;
|
|
306
|
+
}
|
|
307
|
+
st.offset += Buffer.byteLength(chunk.slice(0, lastNl + 1), "utf8");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
try { writeFileSync(sp, JSON.stringify(st)); } catch { /* ignore */ }
|
|
311
|
+
return st.saved > 0 ? fmtSaved(st) : null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Per-session llmlingua. smart-read keys its sessions map by CLAUDE_CODE_SESSION_ID â the same
|
|
315
|
+
// id the statusbar gets on stdin â so we read sessions[sid] (not a single global block).
|
|
316
|
+
// Prefer last_saved_pct; else show the ratio. Flat lifetime-only writers expose no session -> absent.
|
|
317
|
+
function linguaSavings(sid) {
|
|
318
|
+
const d = readJson(llmlinguaPath());
|
|
319
|
+
const s = d && d.sessions && d.sessions[sid];
|
|
320
|
+
if (!s || !(s.tokens_saved > 0)) return null;
|
|
321
|
+
if (s.last_saved_pct != null) return `${k(s.tokens_saved)} ${Math.round(s.last_saved_pct)}%`;
|
|
322
|
+
if (s.last_ratio != null) return `${k(s.tokens_saved)} ${Number(s.last_ratio).toFixed(1)}x`;
|
|
323
|
+
return k(s.tokens_saved);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function statsValues(data, cwd) {
|
|
327
|
+
const v = {};
|
|
328
|
+
const rawSid = String((data && data.session_id) || "default");
|
|
329
|
+
const sid = rawSid.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 48); // filesystem-safe state-file key
|
|
330
|
+
const lean = leanCtxSavings(cwd, sid);
|
|
331
|
+
if (lean) v["save.leanctx"] = lean;
|
|
332
|
+
// smart-read keys sessions[] by the RAW CLAUDE_CODE_SESSION_ID -> look up with the unsanitized id.
|
|
333
|
+
const ling = linguaSavings(rawSid);
|
|
334
|
+
if (ling) v["save.lingua"] = ling;
|
|
335
|
+
return v;
|
|
336
|
+
}
|
|
337
|
+
|
|
231
338
|
const PERM = { plan: "ð plan", auto: "âĐ auto", acceptEdits: "âĐ auto", bypassPermissions: "âĐ bypass" };
|
|
232
339
|
const OK_RGB = [96, 200, 104]; // matches render.js OK (done, green)
|
|
233
340
|
const CRIT_RGB = [224, 84, 64]; // matches render.js CRIT (deleted, red)
|
|
@@ -287,7 +394,7 @@ function main() {
|
|
|
287
394
|
const now = Date.now() / 1000;
|
|
288
395
|
const st = readState(data);
|
|
289
396
|
const cwd = data.cwd || (data.workspace || {}).current_dir;
|
|
290
|
-
const values = { ...buildValues(data), ...activityValues(st, now), ...sysValues(cwd) };
|
|
397
|
+
const values = { ...buildValues(data), ...activityValues(st, now), ...sysValues(cwd), ...statsValues(data, cwd) };
|
|
291
398
|
const [advModel, advTs] = advisorInfo(data.transcript_path || "");
|
|
292
399
|
if (advModel) values["advisor.model"] = advModel;
|
|
293
400
|
const god_until = godFlash(data, advTs, now);
|