@kaydash9999/banker-plugins 0.19.4 → 0.20.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +6 -3
- package/bin/lib/telemetry-config.mjs +16 -3
- package/codex/manifest.json +1 -1
- package/commands/setup.md +1 -1
- package/hooks/update-checkin.mjs +2 -2
- package/package.json +1 -1
- package/skills/payload-mon/SKILL.md +6 -4
- package/skills/payload-mon/scripts/payload-mon.mjs +12 -2
- package/skills/setup-omc-hud/SKILL.md +113 -52
- package/skills/setup-omc-hud/assets/LICENSE-omc_hud +21 -0
- package/skills/setup-omc-hud/assets/apply-hud.mjs +234 -0
- package/skills/setup-omc-hud/assets/hud-config.json +53 -0
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +545 -0
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +11 -2
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OMC HUD custom formatter (wrapper) — v4
|
|
4
|
+
*
|
|
5
|
+
* Runs the canonical OMC HUD (omc-hud.mjs), then reformats its rendered line
|
|
6
|
+
* into a compact pipe-delimited layout requested by the user:
|
|
7
|
+
*
|
|
8
|
+
* <model>/<effort>|5h:<pct>/<reset>|wk:<pct>/<reset>|<mm>:<pct>/<reset>|think|PL:<mb>M|se:<hours>h|ctx:<pct>%|🤖<agents>|🔧<tools>|⚡<skills>|<cwd>|<account>|<branch>|claude<cur>→<new>
|
|
9
|
+
* e.g. Op5.5/high|5h:8%/1h23m|wk:96%/1h23m|Fa:0%/1h23m|think|PL:0.4M|se:0.4h|ctx:12%|🤖1|🔧14|⚡3|...
|
|
10
|
+
*
|
|
11
|
+
* Changes vs v3:
|
|
12
|
+
* - OMC 5.x wraps at the terminal width; the width is hidden from OMC and the
|
|
13
|
+
* wrapper wraps the compact line itself at segment boundaries (no "…").
|
|
14
|
+
* - effort:<level> badge dropped; session -> se:<hours, 1 decimal>h.
|
|
15
|
+
* - ctx:<pct>% comes right after se, without OMC's " COMPRESS?" / " CRITICAL"
|
|
16
|
+
* words (the color shows the level).
|
|
17
|
+
* - OMC's call counts ("🔧14 🤖1 ⚡3") are shown agents, tools, skills, one
|
|
18
|
+
* segment each: 🤖1|🔧14|⚡3. A count OMC leaves out (zero) stays out.
|
|
19
|
+
* - per-model usage badge (fable:0%(1h31m)) -> Fa:0%/1h31m, after the rate windows.
|
|
20
|
+
* - PL:<MB, 1 decimal>M always shown before se. It reads payload-mon's module
|
|
21
|
+
* copy next to this file (hud/payload-mon/payload-size.mjs); without it PL
|
|
22
|
+
* is left out.
|
|
23
|
+
* - cwd group split on "|"; branch shown without "branch:" after the account.
|
|
24
|
+
* - OMC#X.Y.Z label no longer shown, nor OMC's update hint lines
|
|
25
|
+
* ("[!] omc X - paste: ! ...", "[!] claude X - paste: ! claude update").
|
|
26
|
+
* - Claude Code update notice "[Claude#2.1.300] -> 2.1.302 claude update" ->
|
|
27
|
+
* "claude2.1.300→2.1.302", shown last. A claude-update-last block has
|
|
28
|
+
* nothing left to move.
|
|
29
|
+
* - >=85% is pink (256-color 213) instead of red.
|
|
30
|
+
* - Blocks added by `payload-mon on` / `claude-update-last on` are not part of
|
|
31
|
+
* this file; those tools insert them.
|
|
32
|
+
*
|
|
33
|
+
* Why a wrapper: OMC's render.js hardcodes " | " separators, the "[...]" label
|
|
34
|
+
* brackets, the "Model: " prefix, the "percent(time)" rate layout and the
|
|
35
|
+
* "session:Nm" label. None are configurable, so we transform the final text.
|
|
36
|
+
* All data (usage %, reset times, tool counts, cwd) still comes from OMC.
|
|
37
|
+
*
|
|
38
|
+
* Defensive: on any parse failure it prints OMC's raw output unchanged, so the
|
|
39
|
+
* statusline never breaks if OMC's format changes after an update.
|
|
40
|
+
*
|
|
41
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
+
* Changes vs v1 (formatting logic below is byte-identical to v1 on purpose):
|
|
43
|
+
*
|
|
44
|
+
* 1. The canonical HUD runs IN THIS PROCESS instead of via spawnSync, so a
|
|
45
|
+
* statusline refresh costs one node process instead of two. Windows charges
|
|
46
|
+
* kernel paged pool per process creation (fltmgr FMfn name-cache node), and
|
|
47
|
+
* on this machine that accumulation is not reclaimable short of a reboot.
|
|
48
|
+
*
|
|
49
|
+
* 2. Every unbounded wait now has a time budget. v1 could block forever in two
|
|
50
|
+
* places with no timeout: readFileSync(0) waiting for stdin EOF, and
|
|
51
|
+
* spawnSync waiting on the child. Both are SYNCHRONOUS, so a setTimeout
|
|
52
|
+
* watchdog cannot fire while they block — the event loop is not running.
|
|
53
|
+
* A stalled instance never dies, and the next refresh spawns another, so
|
|
54
|
+
* they accumulate linearly. Both are now async and bounded.
|
|
55
|
+
*
|
|
56
|
+
* Timer discipline comes from a 3-OS reproduction documented in
|
|
57
|
+
* .omc/wiki/bounding-a-hanging-dynamic-import-in-node.md:
|
|
58
|
+
* - the budget timer stays REF'd: an unref'd timer never fires when Node bails
|
|
59
|
+
* out of an unsettled top-level await with exit 13,
|
|
60
|
+
* - the self-exit timer is UNREF'd: it must not hold a healthy process open,
|
|
61
|
+
* and when something really is hung the loop is alive so it still fires,
|
|
62
|
+
* - the budget is 10s, not 3s: a tight budget silently discards work that was
|
|
63
|
+
* merely slow, without saving any time (the hung import holds the loop anyway).
|
|
64
|
+
*
|
|
65
|
+
* 🔴 omc-hud.mjs ends with a bare `main();` — no await. Module evaluation
|
|
66
|
+
* therefore completes BEFORE any output is produced. Awaiting the import
|
|
67
|
+
* alone captures nothing (that is why v2 printed "[OMC HUD] no output").
|
|
68
|
+
* We wait for the first complete line instead.
|
|
69
|
+
*/
|
|
70
|
+
import { spawnSync } from "node:child_process";
|
|
71
|
+
import { readFileSync } from "node:fs";
|
|
72
|
+
import { homedir } from "node:os";
|
|
73
|
+
import { dirname, join } from "node:path";
|
|
74
|
+
import { Readable } from "node:stream";
|
|
75
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
76
|
+
|
|
77
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
78
|
+
const HUD = join(__dirname, "omc-hud.mjs");
|
|
79
|
+
|
|
80
|
+
const STDIN_TIMEOUT_MS = 2000; // Claude Code pipes the payload immediately
|
|
81
|
+
const RENDER_TIMEOUT_MS = 10000; // see timer discipline note above
|
|
82
|
+
const SPAWN_TIMEOUT_MS = 8000; // fallback path only
|
|
83
|
+
const EXIT_GRACE_MS = 2000;
|
|
84
|
+
const HARD_EXIT_MS = 20000;
|
|
85
|
+
|
|
86
|
+
// Last-resort guard: if any path leaves us alive past this, end voluntarily.
|
|
87
|
+
// unref'd so a healthy render is never held open by it.
|
|
88
|
+
{
|
|
89
|
+
const g = setTimeout(() => {
|
|
90
|
+
try {
|
|
91
|
+
process.stdout.write("\n");
|
|
92
|
+
} catch {
|
|
93
|
+
/* stdout already gone */
|
|
94
|
+
}
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}, HARD_EXIT_MS);
|
|
97
|
+
if (g && g.unref) g.unref();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Print and terminate. The canonical HUD ran in-process, so its own async work
|
|
101
|
+
// may still be pending on the event loop; once our line is out there is nothing
|
|
102
|
+
// left to wait for. The write callback is the flush-safe exit point, and the
|
|
103
|
+
// unref'd timer covers the case where the callback never arrives.
|
|
104
|
+
// Only the first call prints: the fallback path may emit and then fall through
|
|
105
|
+
// to the formatter, whose passthrough catch would print a second line.
|
|
106
|
+
let emitted = false;
|
|
107
|
+
function emit(s) {
|
|
108
|
+
if (emitted) return;
|
|
109
|
+
emitted = true;
|
|
110
|
+
try {
|
|
111
|
+
process.stdout.write(s, () => process.exit(0));
|
|
112
|
+
} catch {
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
const k = setTimeout(() => process.exit(0), EXIT_GRACE_MS);
|
|
116
|
+
if (k && k.unref) k.unref();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Currently logged-in Claude account: the part of the email before "@".
|
|
120
|
+
// Read from <config home>/.claude.json (oauthAccount.emailAddress) via a light
|
|
121
|
+
// regex so we don't JSON.parse the whole config every render. Claude Code keeps
|
|
122
|
+
// that file in $CLAUDE_CONFIG_DIR when set, otherwise in the home folder.
|
|
123
|
+
function claudeJsonDir() {
|
|
124
|
+
const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
|
|
125
|
+
if (!v) return homedir();
|
|
126
|
+
if (v === "~") return homedir();
|
|
127
|
+
if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
|
|
128
|
+
return v;
|
|
129
|
+
}
|
|
130
|
+
function loginAccount() {
|
|
131
|
+
try {
|
|
132
|
+
const raw = readFileSync(join(claudeJsonDir(), ".claude.json"), "utf8");
|
|
133
|
+
const m = raw.match(/"emailAddress"\s*:\s*"([^"@]+)@/);
|
|
134
|
+
return m ? m[1] : "";
|
|
135
|
+
} catch {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Read the statusline JSON Claude Code pipes on stdin, bounded.
|
|
141
|
+
// A TTY means a human ran this by hand — there is no EOF coming, so skip it
|
|
142
|
+
// entirely rather than hang waiting for one.
|
|
143
|
+
function readStdinBounded(ms) {
|
|
144
|
+
if (process.stdin.isTTY) return Promise.resolve("");
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
let data = "";
|
|
147
|
+
let done = false;
|
|
148
|
+
const onData = (c) => {
|
|
149
|
+
data += c;
|
|
150
|
+
};
|
|
151
|
+
const finish = () => {
|
|
152
|
+
if (done) return;
|
|
153
|
+
done = true;
|
|
154
|
+
clearTimeout(t);
|
|
155
|
+
process.stdin.removeListener("data", onData);
|
|
156
|
+
process.stdin.removeListener("end", finish);
|
|
157
|
+
process.stdin.removeListener("error", finish);
|
|
158
|
+
try {
|
|
159
|
+
process.stdin.pause();
|
|
160
|
+
} catch {
|
|
161
|
+
/* already closed */
|
|
162
|
+
}
|
|
163
|
+
resolve(data);
|
|
164
|
+
};
|
|
165
|
+
const t = setTimeout(finish, ms); // ref'd on purpose
|
|
166
|
+
try {
|
|
167
|
+
process.stdin.setEncoding("utf8");
|
|
168
|
+
process.stdin.on("data", onData);
|
|
169
|
+
process.stdin.once("end", finish);
|
|
170
|
+
process.stdin.once("error", finish);
|
|
171
|
+
} catch {
|
|
172
|
+
finish();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const input = await readStdinBounded(STDIN_TIMEOUT_MS);
|
|
178
|
+
|
|
179
|
+
// Current reasoning effort level (low/medium/high/xhigh) from the statusline
|
|
180
|
+
// payload (effort.level) — shown next to the model. Empty if absent.
|
|
181
|
+
let effort = "";
|
|
182
|
+
try {
|
|
183
|
+
effort = String(JSON.parse(input)?.effort?.level || "");
|
|
184
|
+
} catch {
|
|
185
|
+
/* no/invalid stdin JSON */
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Run the canonical HUD in-process, capturing what it writes to stdout.
|
|
189
|
+
// Resolves on the first complete line (see the `main();` note in the header),
|
|
190
|
+
// or when the budget expires — whichever comes first.
|
|
191
|
+
//
|
|
192
|
+
// 🔴 The HUD DOES read stdin — dist/hud/stdin.js readStdin() async-iterates
|
|
193
|
+
// process.stdin (it is not readFileSync(0)). We already drained the real fd,
|
|
194
|
+
// so without replaying the payload the HUD sees empty input and prints its
|
|
195
|
+
// "HUD installed / statusLine configured" info screen instead of a
|
|
196
|
+
// statusline. Swapping process.stdin for a stream carrying the same bytes
|
|
197
|
+
// restores identical behaviour to the spawnSync path, which fed the child
|
|
198
|
+
// through its stdin.
|
|
199
|
+
// What the HUD writes to stdout is collected instead of printed; `onLine` runs
|
|
200
|
+
// once a full line is in.
|
|
201
|
+
function captureStdout(onLine) {
|
|
202
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
203
|
+
let captured = "";
|
|
204
|
+
process.stdout.write = (chunk, enc, cb) => {
|
|
205
|
+
captured += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
|
|
206
|
+
const done = typeof enc === "function" ? enc : cb;
|
|
207
|
+
if (typeof done === "function") done();
|
|
208
|
+
if (captured.includes("\n")) onLine();
|
|
209
|
+
return true;
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
text: () => captured,
|
|
213
|
+
restore: () => {
|
|
214
|
+
process.stdout.write = origWrite;
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function renderInProcess(ms) {
|
|
220
|
+
const origStdin = Object.getOwnPropertyDescriptor(process, "stdin");
|
|
221
|
+
let settleFirstLine;
|
|
222
|
+
const firstLine = new Promise((r) => {
|
|
223
|
+
settleFirstLine = r;
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const replay = Readable.from(input ? [input] : []);
|
|
227
|
+
replay.isTTY = false; // must stay falsy or readStdin() bails out early
|
|
228
|
+
try {
|
|
229
|
+
Object.defineProperty(process, "stdin", { value: replay, configurable: true });
|
|
230
|
+
} catch {
|
|
231
|
+
/* leave the real stdin in place; the fallback path still works */
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const out = captureStdout(settleFirstLine);
|
|
235
|
+
let budgetTimer;
|
|
236
|
+
const budget = new Promise((r) => {
|
|
237
|
+
budgetTimer = setTimeout(r, ms); // ref'd on purpose
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await Promise.race([
|
|
242
|
+
(async () => {
|
|
243
|
+
await import(pathToFileURL(HUD).href);
|
|
244
|
+
await firstLine;
|
|
245
|
+
})(),
|
|
246
|
+
budget,
|
|
247
|
+
]);
|
|
248
|
+
} finally {
|
|
249
|
+
clearTimeout(budgetTimer);
|
|
250
|
+
out.restore();
|
|
251
|
+
// Restore the real stdin so the spawnSync fallback below behaves normally.
|
|
252
|
+
if (origStdin) {
|
|
253
|
+
try {
|
|
254
|
+
Object.defineProperty(process, "stdin", origStdin);
|
|
255
|
+
} catch {
|
|
256
|
+
/* not restorable; we exit right after anyway */
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out.text();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// OMC 5.x wraps its line at the terminal width (index.js: stderr/stdout
|
|
264
|
+
// columns, then $COLUMNS). Wrapped tails land below the OMC# line, where this
|
|
265
|
+
// wrapper passes them through raw ("session:9m | ctx:12% | 🔧12"). The wrapper
|
|
266
|
+
// does its own compaction, so hide the width and let OMC emit one line.
|
|
267
|
+
// The width is read first: the wrapper wraps the compact line itself at the
|
|
268
|
+
// end, so Claude Code does not cut the tail off with "…".
|
|
269
|
+
const TERM_COLS =
|
|
270
|
+
process.stderr.columns || process.stdout.columns || parseInt(process.env.COLUMNS ?? "0", 10) || 0;
|
|
271
|
+
delete process.env.COLUMNS; // also keeps the spawnSync fallback unwrapped
|
|
272
|
+
for (const s of [process.stdout, process.stderr]) {
|
|
273
|
+
try {
|
|
274
|
+
Object.defineProperty(s, "columns", { value: undefined, configurable: true });
|
|
275
|
+
} catch {
|
|
276
|
+
/* not overridable; OMC may wrap and the tail stays below */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let raw = "";
|
|
281
|
+
try {
|
|
282
|
+
raw = (await renderInProcess(RENDER_TIMEOUT_MS)).replace(/\r/g, "");
|
|
283
|
+
} catch {
|
|
284
|
+
raw = ""; // fall through to the child-process path
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!raw.trim()) {
|
|
288
|
+
// In-process rendering produced nothing usable. Fall back to the original
|
|
289
|
+
// out-of-process path: it costs an extra node, but a statusline that renders
|
|
290
|
+
// beats one that does not — e.g. if a future OMC version stops being
|
|
291
|
+
// import-safe. Bounded, unlike v1.
|
|
292
|
+
const res = spawnSync(process.execPath, [HUD], {
|
|
293
|
+
input,
|
|
294
|
+
encoding: "utf8",
|
|
295
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
296
|
+
killSignal: "SIGKILL",
|
|
297
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
298
|
+
});
|
|
299
|
+
raw = (res.stdout || "").replace(/\r/g, "");
|
|
300
|
+
if (!raw.trim()) {
|
|
301
|
+
emit(((res.stderr || "").trim() || "[OMC HUD] no output") + "\n");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
306
|
+
|
|
307
|
+
// Terminal columns of a string: ANSI is free, CJK and emoji take two columns,
|
|
308
|
+
// variation selectors and ZWJ take none.
|
|
309
|
+
const WRAP_MARGIN = 4;
|
|
310
|
+
// BMP emoji drawn two columns wide (East Asian Width W), e.g. ⚡ in OMC's call counts.
|
|
311
|
+
const WIDE_BMP_EMOJI = [
|
|
312
|
+
[0x231a, 0x231b], [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3],
|
|
313
|
+
[0x25fd, 0x25fe], [0x2614, 0x2615], [0x2648, 0x2653], [0x267f, 0x267f],
|
|
314
|
+
[0x2693, 0x2693], [0x26a1, 0x26a1], [0x26aa, 0x26ab], [0x26bd, 0x26be],
|
|
315
|
+
[0x26c4, 0x26c5], [0x26ce, 0x26ce], [0x26d4, 0x26d4], [0x26ea, 0x26ea],
|
|
316
|
+
[0x26f2, 0x26f3], [0x26f5, 0x26f5], [0x26fa, 0x26fa], [0x26fd, 0x26fd],
|
|
317
|
+
[0x2705, 0x2705], [0x270a, 0x270b], [0x2728, 0x2728], [0x274c, 0x274c],
|
|
318
|
+
[0x274e, 0x274e], [0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797],
|
|
319
|
+
[0x27b0, 0x27b0], [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], [0x2b50, 0x2b50],
|
|
320
|
+
[0x2b55, 0x2b55],
|
|
321
|
+
];
|
|
322
|
+
// Every two-column range: Hangul Jamo, the emoji above, CJK, Hangul syllables, CJK compatibility and
|
|
323
|
+
// forms, fullwidth forms, the emoji planes and the supplementary ideographs.
|
|
324
|
+
const WIDE_RANGES = [
|
|
325
|
+
[0x1100, 0x115f], ...WIDE_BMP_EMOJI, [0x2e80, 0xa4cf], [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe30, 0xfe4f],
|
|
326
|
+
[0xff00, 0xff60], [0xffe0, 0xffe6], [0x1f300, 0x1faff], [0x20000, 0x10ffff],
|
|
327
|
+
];
|
|
328
|
+
const isWide = (c) => WIDE_RANGES.some(([lo, hi]) => c >= lo && c <= hi);
|
|
329
|
+
const visibleWidth = (s) => {
|
|
330
|
+
let w = 0;
|
|
331
|
+
for (const ch of stripAnsi(s)) {
|
|
332
|
+
const c = ch.codePointAt(0);
|
|
333
|
+
if (c === 0x200d || (c >= 0xfe00 && c <= 0xfe0f)) continue;
|
|
334
|
+
w += isWide(c) ? 2 : 1;
|
|
335
|
+
}
|
|
336
|
+
return w;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Color helpers — we strip OMC's ANSI to parse, then re-apply our own so the
|
|
340
|
+
// statusline is highlighted. Threshold scale (usage/context %): green<70,
|
|
341
|
+
// yellow<85, pink>=85 (pink replaces red on request). Pipes and path are
|
|
342
|
+
// dimmed; model name cyan, effort yellow.
|
|
343
|
+
const A = (c, s) => `\x1b[${c}m${s}\x1b[0m`;
|
|
344
|
+
const SEP = "\x1b[2m|\x1b[0m";
|
|
345
|
+
const PINK = "38;5;213";
|
|
346
|
+
const pc = (n) => (n >= 85 ? PINK : n >= 70 ? "33" : "32");
|
|
347
|
+
const colorRate = (seg) => {
|
|
348
|
+
const m = seg.match(/^(\w+):(\d+)%\/(.+)$/);
|
|
349
|
+
return m ? `${m[1]}:${A(pc(+m[2]), m[2] + "%")}\x1b[2m/\x1b[0m${A("36", m[3])}` : seg;
|
|
350
|
+
};
|
|
351
|
+
const colorCtx = (seg) => {
|
|
352
|
+
const m = seg.match(/^ctx:(\d+)%$/);
|
|
353
|
+
return m ? `ctx:${A(pc(+m[1]), m[1] + "%")}` : seg;
|
|
354
|
+
};
|
|
355
|
+
const colorModel = (seg) => {
|
|
356
|
+
const m = seg.match(/^(.+?)\/(\w+)$/);
|
|
357
|
+
return m ? `${A("36", m[1])}\x1b[2m/\x1b[0m${A("33", m[2])}` : A("36", seg);
|
|
358
|
+
};
|
|
359
|
+
const colorSe = (seg) => {
|
|
360
|
+
const m = seg.match(/^se:(.+)$/);
|
|
361
|
+
return m ? `se:${A("33", m[1])}` : seg;
|
|
362
|
+
};
|
|
363
|
+
// Per-model usage badge ("fable:0%") rides along in the rate group and has no
|
|
364
|
+
// dedicated field. Shorten the model name to two letters so it matches the
|
|
365
|
+
// model token's own convention (Opus 4.8 -> Op4.8): fable:0% -> Fa:0%.
|
|
366
|
+
// OMC appends a stale marker and/or a reset time to scoped weekly buckets
|
|
367
|
+
// (renderRateLimits in dist/hud/elements/limits.js), so the pattern must not
|
|
368
|
+
// anchor at "%": keep that tail intact -> fable:0%(3d3h) -> Fa:0%(3d3h).
|
|
369
|
+
const shortModelPct = (seg) => {
|
|
370
|
+
const m = seg.match(/^([A-Za-z][A-Za-z0-9.-]*):(\d+%\*?(?:\([^)]*\))?)$/);
|
|
371
|
+
return m ? `${m[1][0].toUpperCase()}${m[1].slice(1, 2).toLowerCase()}:${m[2]}` : seg;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const lines = raw.split("\n").map(stripAnsi).filter((l) => l.length);
|
|
376
|
+
const mainIdx = lines.findIndex((l) => l.includes("OMC#"));
|
|
377
|
+
if (mainIdx === -1) throw new Error("no OMC line");
|
|
378
|
+
|
|
379
|
+
const main = lines[mainIdx];
|
|
380
|
+
const pathLine = lines.slice(0, mainIdx).join(" ").trim(); // cwd (+git) group above
|
|
381
|
+
// Detail lines (agent tree, warnings) are kept as-is, except OMC's update
|
|
382
|
+
// hints ("[!] omc 5.7.5 - paste: ! claude plugin ...", "[!] claude 2.1.302 -
|
|
383
|
+
// paste: ! claude update"): the OMC version is not wanted here, and the Claude
|
|
384
|
+
// Code update already shows as claude<cur>→<new> on the main line.
|
|
385
|
+
const UPDATE_HINT = /^\[!\] (?:omc|claude) \S+ - paste: /;
|
|
386
|
+
const below = lines.slice(mainIdx + 1).filter((l) => !UPDATE_HINT.test(l.trim()));
|
|
387
|
+
|
|
388
|
+
// Collapse " | " -> "|", then split into segments.
|
|
389
|
+
const rawSegs = main
|
|
390
|
+
.replace(/\s*\|\s*/g, "|")
|
|
391
|
+
.split("|")
|
|
392
|
+
.map((s) => s.trim())
|
|
393
|
+
.filter(Boolean);
|
|
394
|
+
|
|
395
|
+
// The 5h/wk/sn rate windows live in one space-separated segment; expand them.
|
|
396
|
+
// The per-model usage badge ("fable:0%") arrives inside that same segment, so
|
|
397
|
+
// remember which pieces came from there. Only those get abbreviated later —
|
|
398
|
+
// unrelated badges that also land in `extra` must be left alone.
|
|
399
|
+
const segs = [];
|
|
400
|
+
const rateGroup = new Set();
|
|
401
|
+
// OMC's Claude Code update notice ("[Claude#2.1.300] -> 2.1.302 claude update",
|
|
402
|
+
// render.js claudeLabel) is shortened to "claude2.1.300→2.1.302" and shown last.
|
|
403
|
+
// It is taken out of segs, so a claude-update-last block (which looks for the
|
|
404
|
+
// long form in segs) finds nothing to move.
|
|
405
|
+
const UPDATE_NOTICE = /^\[Claude(?:#([^\]]*))?\]\s*->\s*(\S+)\s+claude\s+update\b/;
|
|
406
|
+
let update = "";
|
|
407
|
+
for (const s of rawSegs) {
|
|
408
|
+
const um = s.match(UPDATE_NOTICE);
|
|
409
|
+
if (um) {
|
|
410
|
+
update = `claude${um[1] || ""}→${um[2]}`;
|
|
411
|
+
} else if (/\b(5h|wk|sn):/.test(s) && /\s/.test(s)) {
|
|
412
|
+
for (const p of s.split(/\s+/)) {
|
|
413
|
+
if (!p) continue;
|
|
414
|
+
segs.push(p);
|
|
415
|
+
if (!/^(5h|wk|sn):/.test(p)) rateGroup.add(p);
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
segs.push(s);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Without effort.level on stdin, take the level from OMC's effort badge.
|
|
423
|
+
if (!effort) {
|
|
424
|
+
const badge = segs.find((s) => /^effort:\S/.test(s));
|
|
425
|
+
if (badge) effort = badge.slice("effort:".length).trim();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const f = { label: "", model: "", r5: "", rwk: "", rsn: "", think: "", ctx: "", se: "", counts: "" };
|
|
429
|
+
let ctxShown = ""; // the ctx segment shown after se; f.ctx stays empty (see the ctx line below)
|
|
430
|
+
const extra = [];
|
|
431
|
+
const modelRates = []; // per-model usage badges, shown right after the rate windows
|
|
432
|
+
const slash = (s) => s.replace(/%\(([^)]+)\)/, "%/$1"); // 6%(3h56m) -> 6%/3h56m
|
|
433
|
+
|
|
434
|
+
for (const s of segs) {
|
|
435
|
+
if (/^\[?OMC#/.test(s)) {
|
|
436
|
+
const m = s.match(/OMC#([0-9][0-9.]*)/);
|
|
437
|
+
f.label = m ? `OMC#${m[1]}` : s; // drop brackets + trailing "L"
|
|
438
|
+
} else if (/^Model:/.test(s) || /^(Opus|Sonnet|Haiku)\b/.test(s)) {
|
|
439
|
+
const m = s.match(/(?:Model:\s*)?([A-Za-z]+)\s*([0-9][0-9.]*)/);
|
|
440
|
+
// Opus 4.8 -> Op4.8/high; an unknown id (Model: claude-x-5) keeps its name.
|
|
441
|
+
const name = m ? `${m[1].slice(0, 2)}${m[2]}` : s.replace(/^Model:\s*/, "");
|
|
442
|
+
f.model = `${name}${effort ? "/" + effort : ""}`;
|
|
443
|
+
} else if (/^5h:/.test(s)) {
|
|
444
|
+
f.r5 = slash(s);
|
|
445
|
+
} else if (/^wk:/.test(s)) {
|
|
446
|
+
f.rwk = slash(s);
|
|
447
|
+
} else if (/^sn:/.test(s)) {
|
|
448
|
+
f.rsn = slash(s);
|
|
449
|
+
} else if (/^think/i.test(s)) {
|
|
450
|
+
f.think = "think";
|
|
451
|
+
} else if (/^effort:/.test(s)) {
|
|
452
|
+
// OMC 5.x effort badge; the level already rides on the model (Op5.5/high).
|
|
453
|
+
} else if (/^ctx:/.test(s)) {
|
|
454
|
+
// "ctx:82% COMPRESS?" -> "ctx:82%": the color already says how full it is.
|
|
455
|
+
const m = s.match(/^ctx:(\d+)%/);
|
|
456
|
+
ctxShown = m ? `ctx:${m[1]}%` : s;
|
|
457
|
+
} else if (/^session:/.test(s)) {
|
|
458
|
+
const m = s.match(/session:(\d+)m/);
|
|
459
|
+
f.se = m ? `se:${(parseInt(m[1], 10) / 60).toFixed(1)}h` : s.replace(/^session:/, "se:");
|
|
460
|
+
} else if (/🔧|🤖|⚡/.test(s) || /^[TAS]:\d/.test(s)) {
|
|
461
|
+
f.counts = s;
|
|
462
|
+
} else {
|
|
463
|
+
if (rateGroup.has(s)) modelRates.push(slash(shortModelPct(s))); // fable:0%(1h31m) -> Fa:0%/1h31m
|
|
464
|
+
else extra.push(s); // dynamic badges: todos, ralph, autopilot, bg, skill, etc.
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const colored = [];
|
|
469
|
+
if (f.model) colored.push(colorModel(f.model));
|
|
470
|
+
if (f.r5) colored.push(colorRate(f.r5));
|
|
471
|
+
if (f.rwk) colored.push(colorRate(f.rwk));
|
|
472
|
+
if (f.rsn) colored.push(colorRate(f.rsn));
|
|
473
|
+
for (const r of modelRates) colored.push(colorRate(r));
|
|
474
|
+
if (f.think) colored.push(A("36", f.think));
|
|
475
|
+
// Keep the next line: f.ctx is always empty now (ctx is shown after se), but
|
|
476
|
+
// `payload-mon on` inserts its block right after this exact line, ahead of PL.
|
|
477
|
+
if (f.ctx) colored.push(colorCtx(f.ctx));
|
|
478
|
+
// PL: payload size always shown before se, as "PL:0.3M" (MB, one decimal),
|
|
479
|
+
// colored by its share of the 32MB limit. Replaces the payload-mon segment,
|
|
480
|
+
// which only appears from 8MB on. Reuses payload-mon's module copy and cache;
|
|
481
|
+
// without it (payload-mon off) or without a transcript, PL is left out.
|
|
482
|
+
try {
|
|
483
|
+
const transcript = JSON.parse(input)?.transcript_path;
|
|
484
|
+
if (transcript) {
|
|
485
|
+
const { estimatePayloadBytes, REQUEST_LIMIT_BYTES } = await import(pathToFileURL(join(__dirname, "payload-mon", "payload-size.mjs")).href);
|
|
486
|
+
const bytes = estimatePayloadBytes(transcript);
|
|
487
|
+
if (Number.isFinite(bytes)) {
|
|
488
|
+
for (let i = colored.length - 1; i >= 0; i--) if (/^payload:/.test(stripAnsi(colored[i]))) colored.splice(i, 1);
|
|
489
|
+
const pct = Math.floor((bytes / REQUEST_LIMIT_BYTES) * 100);
|
|
490
|
+
colored.push(`PL:${A(pc(pct), (bytes / (1024 * 1024)).toFixed(1) + "M")}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} catch {
|
|
494
|
+
/* no stdin JSON, unreadable transcript or module missing */
|
|
495
|
+
}
|
|
496
|
+
if (f.se) colored.push(colorSe(f.se));
|
|
497
|
+
if (ctxShown) colored.push(colorCtx(ctxShown));
|
|
498
|
+
// OMC prints "🔧14 🤖1 ⚡3" ("T:14 A:1 S:3" in ASCII) and leaves out a zero count.
|
|
499
|
+
// Show agents, tools, skills, one segment each; anything else keeps its place after them.
|
|
500
|
+
if (f.counts) {
|
|
501
|
+
const rank = (p) => (/^(?:🤖|A:)/u.test(p) ? 0 : /^(?:🔧|T:)/u.test(p) ? 1 : /^(?:⚡|S:)/u.test(p) ? 2 : 3);
|
|
502
|
+
colored.push(...f.counts.trim().split(/\s+/).sort((a, b) => rank(a) - rank(b)));
|
|
503
|
+
}
|
|
504
|
+
for (const e of extra) colored.push(A("2", e));
|
|
505
|
+
// OMC's cwd group is "cwd | branch:x | ...": keep the cwd parts here with "|",
|
|
506
|
+
// and move the branch (without its "branch:" label) right before OMC#.
|
|
507
|
+
let branch = "";
|
|
508
|
+
const pathParts = pathLine
|
|
509
|
+
.split(/\s*\|\s*/)
|
|
510
|
+
.map((p) => p.trim())
|
|
511
|
+
.filter((p) => {
|
|
512
|
+
const m = p.match(/^branch:\s*(.+)$/);
|
|
513
|
+
if (m) branch = m[1];
|
|
514
|
+
return p && !m;
|
|
515
|
+
});
|
|
516
|
+
for (const p of pathParts) colored.push(A("36", p));
|
|
517
|
+
const acct = loginAccount();
|
|
518
|
+
if (acct) colored.push(A("92", acct));
|
|
519
|
+
if (branch) colored.push(A("36", branch));
|
|
520
|
+
// The OMC#X.Y.Z label is parsed (it marks OMC's main line) but not shown.
|
|
521
|
+
if (update) colored.push(A("2", update));
|
|
522
|
+
|
|
523
|
+
let result = colored.join(SEP);
|
|
524
|
+
// Wrap at segment boundaries instead of letting Claude Code truncate the tail.
|
|
525
|
+
// WRAP_MARGIN leaves room for the indent Claude Code puts before the line.
|
|
526
|
+
if (TERM_COLS > WRAP_MARGIN + 20) {
|
|
527
|
+
const max = TERM_COLS - WRAP_MARGIN;
|
|
528
|
+
const rows = [];
|
|
529
|
+
let row = "";
|
|
530
|
+
for (const seg of colored) {
|
|
531
|
+
if (row && visibleWidth(row) + 1 + visibleWidth(seg) > max) {
|
|
532
|
+
rows.push(row);
|
|
533
|
+
row = seg;
|
|
534
|
+
} else {
|
|
535
|
+
row = row ? row + SEP + seg : seg;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (row) rows.push(row);
|
|
539
|
+
result = rows.join("\n");
|
|
540
|
+
}
|
|
541
|
+
if (below.length) result += "\n" + below.join("\n");
|
|
542
|
+
emit(result + "\n");
|
|
543
|
+
} catch {
|
|
544
|
+
emit(raw.endsWith("\n") ? raw : raw + "\n"); // safe passthrough
|
|
545
|
+
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { spawnSync } from "node:child_process";
|
|
29
29
|
import { accessSync, constants, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
30
30
|
import { homedir, tmpdir } from "node:os";
|
|
31
|
-
import { join } from "node:path";
|
|
31
|
+
import { join, resolve } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
|
|
34
34
|
const OPEN = "// >>> claude-update-last >>>";
|
|
@@ -42,7 +42,16 @@ const JOIN_LINE = /^ {2}let result = colored\.join\(SEP\);\r?$/gm;
|
|
|
42
42
|
// What the block reads at that line, all in the wrapper's own scope there.
|
|
43
43
|
const NAMES = [/\bconst segs\b/, /\bconst colored\b/, /\bconst stripAnsi\b/, /\bconst A = /];
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
// The same rules as ../assets/apply-hud.mjs (OMC's getClaudeConfigDir): trim, expand a leading "~",
|
|
46
|
+
// make it absolute, drop a trailing separator, so this finds the wrapper the installer wrote.
|
|
47
|
+
function configDir() {
|
|
48
|
+
const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
|
|
49
|
+
if (!v) return join(homedir(), ".claude");
|
|
50
|
+
if (v === "~") return homedir();
|
|
51
|
+
if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
|
|
52
|
+
return resolve(v);
|
|
53
|
+
}
|
|
54
|
+
const CONFIG_DIR = configDir().replace(/[\\/]+$/, "");
|
|
46
55
|
|
|
47
56
|
const DEFAULTS = {
|
|
48
57
|
hudFile: process.env.CLAUDE_UPDATE_LAST_HUD_FILE || join(CONFIG_DIR, "hud", "omc-hud-custom.mjs"),
|