@kaydash9999/banker-plugins 0.20.2 → 0.20.5
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 +179 -1
- package/PRIVACY.md +13 -3
- package/README.md +25 -2
- package/bin/banker.js +84 -40
- package/bin/lib/telemetry-config.mjs +31 -16
- package/codex/manifest.json +1 -1
- package/codex/transform-matrix.md +1 -1
- package/commands/setup.md +23 -0
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +2 -9
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/deep-research/SKILL.md +5 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lineage/SKILL.md +12 -12
- package/skills/lineage/lineage.py +399 -300
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +1 -1
- package/skills/obsidizer/obsidize.mjs +8 -1
- package/skills/omc-patch/SKILL.md +80 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +4 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +9 -6
- package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +4 -3
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +1 -0
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -19,13 +19,16 @@
|
|
|
19
19
|
* column when Node reports one but never quoting it; one holding only white
|
|
20
20
|
* space is read as {}, as Claude Code does; UTF-8 BOM accepted; CLAUDE_CONFIG_DIR trimmed, "~" expanded
|
|
21
21
|
* and made absolute; formatter and HUD config backed up when they differ (the
|
|
22
|
-
* blocks payload-mon and claude-update-last add do not count), settings.json only
|
|
22
|
+
* blocks payload-mon and claude-update-last add do not count unless edited by hand), settings.json only
|
|
23
23
|
* when it changes; pre-releases sort below their release; no
|
|
24
24
|
* folder is made when OMC is missing. Also refused before any change: a config
|
|
25
25
|
* folder path the statusLine shell would act on, a file or folder it cannot
|
|
26
26
|
* write, an OMC folder without both loader files, a relative OMC_PLUGIN_ROOT.
|
|
27
27
|
* Backups keep the original bytes and mode; the files it writes are 0644 and new
|
|
28
|
-
* folders 0755; the other statusLine fields (padding, ...) stay.
|
|
28
|
+
* folders 0755; the other statusLine fields (padding, ...) stay. settings.json goes
|
|
29
|
+
* in as a new file through a rename (a link keeps pointing at it), in place where
|
|
30
|
+
* that cannot be done or would part a hard link or change its owner or group, and
|
|
31
|
+
* is left as it was when the disk is too full for the new file.
|
|
29
32
|
* At the end it runs the installed formatter once with no input and reports
|
|
30
33
|
* whether OMC's HUD loads and which version draws (the render check); that never
|
|
31
34
|
* changes the exit code. APPLY_HUD_CHECK_TIMEOUT_MS (tests) shortens its 25s limit.
|
|
@@ -38,9 +41,13 @@ import {
|
|
|
38
41
|
constants,
|
|
39
42
|
copyFileSync,
|
|
40
43
|
existsSync,
|
|
44
|
+
lstatSync,
|
|
41
45
|
mkdirSync,
|
|
42
46
|
readdirSync,
|
|
43
47
|
readFileSync,
|
|
48
|
+
realpathSync,
|
|
49
|
+
renameSync,
|
|
50
|
+
rmSync,
|
|
44
51
|
statSync,
|
|
45
52
|
writeFileSync,
|
|
46
53
|
} from "node:fs";
|
|
@@ -70,6 +77,27 @@ const configDst = join(OMC_DIR, "hud-config.json");
|
|
|
70
77
|
// PowerShell on Windows) that treats backslashes as escapes.
|
|
71
78
|
const cmdPath = wrapperDst.split(sep).join("/");
|
|
72
79
|
const log = (m) => process.stdout.write(m + "\n");
|
|
80
|
+
// Characters that could act on a terminal or pass for a line of their own are shown as "?", as
|
|
81
|
+
// omc-patch.mjs (UNSAFE_CH) shows file values in output an agent reads.
|
|
82
|
+
const UNSAFE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Co}\p{Cn}\p{Variation_Selector}\p{Default_Ignorable_Code_Point}\u{2800}\u{a0}\u{1680}\u{2000}-\u{200a}\u{202f}\u{205f}\u{3000}]/gu;
|
|
83
|
+
const shown = (s) => String(s).replace(UNSAFE, "?");
|
|
84
|
+
// The old statusLine command as the report shows it: its first word (the program; a NAME=value it sets is skipped) and
|
|
85
|
+
// its length. settings.json can hold tokens (see step 0) and the line goes into a report; masking the parts that look
|
|
86
|
+
// like secrets missed every form no rule named (curl -u, sshpass -p, mysql -pPW), so the rest stays in the backup.
|
|
87
|
+
const ASSIGN = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
88
|
+
// Shell words: runs of plain characters, quoted pieces and backslash escapes (a quote or a space after a backslash
|
|
89
|
+
// stays in its word, inside double quotes too), a quote left open running to the end, so a value in quotes stays one
|
|
90
|
+
// word with its name.
|
|
91
|
+
const WORDS = /(?:"(?:[^"\\]|\\.)*"?|'[^']*'?|\\[\s\S]?|[^\s"'\\])+/g;
|
|
92
|
+
function priorShown(command) {
|
|
93
|
+
const text = String(command).trim();
|
|
94
|
+
const words = text.match(WORDS) || [];
|
|
95
|
+
const first = words.findIndex((w) => !ASSIGN.test(w));
|
|
96
|
+
// a first word that looks like a setting or an address may hold a secret itself
|
|
97
|
+
const name = first === -1 ? "(no program)" : /[=@]|:\/\//.test(words[first]) ? "(program hidden)" : words[first];
|
|
98
|
+
const head = `${name}${first !== -1 && first < words.length - 1 ? " …" : ""}`;
|
|
99
|
+
return `${JSON.stringify(shown(head))} (${text.length} characters)`;
|
|
100
|
+
}
|
|
73
101
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
74
102
|
const refuse = (lines) => {
|
|
75
103
|
for (const l of lines) log(l);
|
|
@@ -108,6 +136,30 @@ function readSettings(raw) {
|
|
|
108
136
|
if (value && typeof value === "object" && !Array.isArray(value)) return { settings: value, empty: false };
|
|
109
137
|
return { refusal: `[x] settings.json holds ${kindOf(value)}, not a JSON object; nothing changed. Fix it and re-run.` };
|
|
110
138
|
}
|
|
139
|
+
// A target that is there but no regular file (a folder, a FIFO, a device), or a folder the install writes into that
|
|
140
|
+
// is there but no folder, is refused before anything is read or written: reading or writing a FIFO waits for good,
|
|
141
|
+
// and the others stopped the install half-way. Links are followed, as the reads and writes follow them; a target
|
|
142
|
+
// that is not there (a broken link too) is written new.
|
|
143
|
+
function oddTarget() {
|
|
144
|
+
for (const p of [settingsPath, loaderDst, libDst, wrapperDst, configDst]) {
|
|
145
|
+
try {
|
|
146
|
+
if (!statSync(p).isFile()) return `${shown(p)} is not a regular file (a folder, a FIFO or a device)`;
|
|
147
|
+
} catch {
|
|
148
|
+
/* not there: written new */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const d of [CONFIG_DIR, HUD_DIR, join(HUD_DIR, "lib"), OMC_DIR]) {
|
|
152
|
+
try {
|
|
153
|
+
if (!statSync(d).isDirectory()) return `${shown(d)} is not a folder`;
|
|
154
|
+
} catch {
|
|
155
|
+
/* not there: made */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
const odd = oddTarget();
|
|
161
|
+
if (odd) refuse([`[x] ${odd}; nothing changed. Move it aside and re-run.`]);
|
|
162
|
+
|
|
111
163
|
let settings = {};
|
|
112
164
|
let settingsRaw = null;
|
|
113
165
|
if (existsSync(settingsPath)) {
|
|
@@ -199,17 +251,82 @@ function writeOwned(file, data) {
|
|
|
199
251
|
}
|
|
200
252
|
|
|
201
253
|
// The marker blocks payload-mon and claude-update-last add to the installed formatter, matched as those
|
|
202
|
-
// tools match them (payload-mon.mjs BLOCKS, claude-update-last.mjs
|
|
203
|
-
//
|
|
254
|
+
// tools match them (payload-mon.mjs BLOCKS, claude-update-last.mjs BLOCKS). Read as latin1, which maps
|
|
255
|
+
// every byte to one character, so text outside the blocks compares byte for byte.
|
|
204
256
|
const TOOL_BLOCKS = [
|
|
205
257
|
/^[ \t]*\/\/ >>> payload-mon >>>[\s\S]*?\/\/ <<< payload-mon <<<[ \t]*\r?\n/gm,
|
|
206
258
|
/^[ \t]*\/\/ >>> claude-update-last >>>(?:(?!\/\/ >>> claude-update-last >>>)[\s\S])*?\/\/ <<< claude-update-last <<<[ \t]*(?:\r?\n|(?![\s\S]))/gm,
|
|
207
259
|
];
|
|
208
|
-
|
|
260
|
+
// payload-mon's closing line (payload-mon.mjs CLOSE_LINE). No block reaches past the last one, so its
|
|
261
|
+
// pattern runs only on the text before it: otherwise every stray opening line scans to the end again
|
|
262
|
+
// (a 2MB file of them took 28s).
|
|
263
|
+
const PM_CLOSE = /\/\/ <<< payload-mon <<<[ \t]*\r?\n/g;
|
|
264
|
+
// The text without the tools' blocks, and the blocks it held.
|
|
265
|
+
function splitToolBlocks(text) {
|
|
266
|
+
const blocks = [];
|
|
267
|
+
const take = (t, pattern) =>
|
|
268
|
+
t.replace(pattern, (block) => {
|
|
269
|
+
blocks.push(block);
|
|
270
|
+
return "";
|
|
271
|
+
});
|
|
272
|
+
let end = -1;
|
|
273
|
+
for (const m of text.matchAll(PM_CLOSE)) end = m.index + m[0].length;
|
|
274
|
+
const rest = end < 0 ? text : take(text.slice(0, end), TOOL_BLOCKS[0]) + text.slice(end);
|
|
275
|
+
return { body: take(rest, TOOL_BLOCKS[1]), blocks };
|
|
276
|
+
}
|
|
277
|
+
// The blocks as the tools write them (payload-mon.mjs placeBlock, claude-update-last.mjs blockText), with
|
|
278
|
+
// LF line endings and payload-mon's import of its module copy as "<module copy>" (where the copy sits is the
|
|
279
|
+
// tool's to choose; a URL to any other file is an edit). A block that differs from both was edited by hand. When a tool's block changes, the test "payload-mon 블록만 다른 래퍼와 바뀌지 않은 settings.json 은
|
|
280
|
+
// 다시 설치해도 백업하지 않음" fails until this copy follows.
|
|
281
|
+
const TOOL_BLOCK_TEXTS = [
|
|
282
|
+
[
|
|
283
|
+
" // >>> payload-mon >>>",
|
|
284
|
+
" // payload-mon: estimated API request payload against the 32MB request limit, shown",
|
|
285
|
+
" // like ctx from 8MB (25%) on. `payload-mon off` removes this block; a missing or",
|
|
286
|
+
" // broken module costs only this segment, never the statusline.",
|
|
287
|
+
" try {",
|
|
288
|
+
" const transcript = JSON.parse(input)?.transcript_path;",
|
|
289
|
+
" if (transcript) {",
|
|
290
|
+
" const { estimatePayloadBytes, payloadSegment } = await <module copy>;",
|
|
291
|
+
" const segment = payloadSegment(estimatePayloadBytes(transcript));",
|
|
292
|
+
" if (segment) colored.push(colorRate(segment));",
|
|
293
|
+
" }",
|
|
294
|
+
" } catch {",
|
|
295
|
+
" /* no stdin JSON, unreadable transcript or module error */",
|
|
296
|
+
" }",
|
|
297
|
+
" // <<< payload-mon <<<",
|
|
298
|
+
"",
|
|
299
|
+
].join("\n"),
|
|
300
|
+
[
|
|
301
|
+
" // >>> claude-update-last >>>",
|
|
302
|
+
" // claude-update-last (setup-omc-hud): OMC's Claude Code update notice",
|
|
303
|
+
" // (\"[Claude#X] -> Y claude update\") goes after every other segment.",
|
|
304
|
+
" // `claude-update-last off` removes this block; on any error the order stays as it was.",
|
|
305
|
+
" try {",
|
|
306
|
+
" const notice = /^\\[Claude(?:#[^\\]]*)?\\]\\s*->\\s*\\S+\\s+claude\\s+update\\b/;",
|
|
307
|
+
" const plain = (s) => stripAnsi(String(s)).trim();",
|
|
308
|
+
" const found = segs.find((s) => notice.test(plain(s)));",
|
|
309
|
+
" if (found) {",
|
|
310
|
+
" const moved = A(\"2\", plain(found));",
|
|
311
|
+
" for (let i = colored.length - 1; i >= 0; i--) if (notice.test(plain(colored[i]))) colored.splice(i, 1);",
|
|
312
|
+
" colored.push(moved);",
|
|
313
|
+
" }",
|
|
314
|
+
" } catch {",
|
|
315
|
+
" /* keep the order the wrapper chose */",
|
|
316
|
+
" }",
|
|
317
|
+
" // <<< claude-update-last <<<",
|
|
318
|
+
"",
|
|
319
|
+
].join("\n"),
|
|
320
|
+
];
|
|
321
|
+
const toolMade = (block) =>
|
|
322
|
+
TOOL_BLOCK_TEXTS.includes(block.replace(/\r\n/g, "\n").replace(/await import\("file:\/\/[^"\\]*\/payload-mon\/payload-size\.mjs"\)/, "await <module copy>"));
|
|
209
323
|
const sameBytes = (a, b) => a.equals(b);
|
|
210
|
-
//
|
|
211
|
-
// needs no claude-update-last block.
|
|
212
|
-
|
|
324
|
+
// Blocks as the tools write them are no reason for a backup: `payload-mon on` puts its block back, and
|
|
325
|
+
// wrapper v4 needs no claude-update-last block. A block edited by hand is one.
|
|
326
|
+
function sameFormatter(a, b) {
|
|
327
|
+
const old = splitToolBlocks(a.toString("latin1"));
|
|
328
|
+
return old.body === splitToolBlocks(b.toString("latin1")).body && old.blocks.every(toolMade);
|
|
329
|
+
}
|
|
213
330
|
|
|
214
331
|
// Keep a timestamped copy, with its bytes and mode, of a file that differs from what replaces it.
|
|
215
332
|
function backupIfDifferent(dst, next, label, same = sameBytes) {
|
|
@@ -234,7 +351,7 @@ function installLoader() {
|
|
|
234
351
|
|
|
235
352
|
// 3. Install the custom formatter and HUD config from the bundled templates.
|
|
236
353
|
// Files that differ from the templates (local edits) are kept as timestamped
|
|
237
|
-
// backups first; the blocks payload-mon / claude-update-last add do not count.
|
|
354
|
+
// backups first; the blocks payload-mon / claude-update-last add, as they write them, do not count.
|
|
238
355
|
function installTemplates() {
|
|
239
356
|
const wrapperSrc = readFileSync(join(SELF, "omc-hud-custom.mjs"));
|
|
240
357
|
const configSrc = readFileSync(join(SELF, "hud-config.json"));
|
|
@@ -246,6 +363,72 @@ function installTemplates() {
|
|
|
246
363
|
log("[ok] HUD config -> " + configDst);
|
|
247
364
|
}
|
|
248
365
|
|
|
366
|
+
// settings.json goes in as a new file through a rename, so a stop midway never leaves it half written.
|
|
367
|
+
// One that is a link (dotfiles) keeps its link, and the file it points at is the one replaced. A file
|
|
368
|
+
// left at the temporary name (an earlier run with this pid, or a link put there) is removed first and
|
|
369
|
+
// never written through. It is written in place as before where the new file cannot go in (a folder
|
|
370
|
+
// this account may not write, a file another program holds open, a file system that refuses the
|
|
371
|
+
// rename) or the link leads nowhere yet, and where a rename would part a hard link (its other name
|
|
372
|
+
// would keep the old text) or the new file has another owner or group. Only a full disk stops it, as
|
|
373
|
+
// writing in place could then cut settings.json short; the new file is written before that choice, so
|
|
374
|
+
// a hard link or another owner finds a full disk first too.
|
|
375
|
+
const FULL_DISK = new Set(["ENOSPC", "EDQUOT"]);
|
|
376
|
+
const isLink = (p) => {
|
|
377
|
+
try {
|
|
378
|
+
return lstatSync(p).isSymbolicLink();
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
const removeTmp = (tmp) => {
|
|
384
|
+
try {
|
|
385
|
+
rmSync(tmp, { force: true });
|
|
386
|
+
} catch {
|
|
387
|
+
/* a folder at that name: left alone */
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
// Whether the file, as `old` was, must keep its inode: it has a second name (a hard link), or the new file
|
|
391
|
+
// `tmp` got another owner or group than it had.
|
|
392
|
+
function keepsInode(old, tmp) {
|
|
393
|
+
if (!old) return false;
|
|
394
|
+
const now = statSync(tmp);
|
|
395
|
+
return old.nlink > 1 || now.uid !== old.uid || now.gid !== old.gid;
|
|
396
|
+
}
|
|
397
|
+
function writeSettings(file, text) {
|
|
398
|
+
let target = file;
|
|
399
|
+
let old;
|
|
400
|
+
let mode;
|
|
401
|
+
try {
|
|
402
|
+
target = realpathSync(file);
|
|
403
|
+
old = statSync(target);
|
|
404
|
+
mode = old.mode & 0o777;
|
|
405
|
+
} catch {
|
|
406
|
+
if (isLink(file)) return writeFileSync(file, text);
|
|
407
|
+
}
|
|
408
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
409
|
+
try {
|
|
410
|
+
rmSync(tmp, { force: true });
|
|
411
|
+
writeFileSync(tmp, text, { flag: "wx", mode });
|
|
412
|
+
if (mode !== undefined && process.platform !== "win32") chmodSync(tmp, mode);
|
|
413
|
+
if (!keepsInode(old, tmp)) return renameSync(tmp, target);
|
|
414
|
+
} catch (e) {
|
|
415
|
+
if (FULL_DISK.has(e.code)) throw e;
|
|
416
|
+
} finally {
|
|
417
|
+
removeTmp(tmp);
|
|
418
|
+
}
|
|
419
|
+
writeFileSync(file, text);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// How many other names settings.json has (a hard link, a `cp -al` snapshot, say), 0 when it cannot be read.
|
|
423
|
+
// writeSettings writes such a file in place, so those names show the new text too.
|
|
424
|
+
function otherNames(file) {
|
|
425
|
+
try {
|
|
426
|
+
return statSync(file).nlink - 1;
|
|
427
|
+
} catch {
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
249
432
|
// 4. Point settings.json statusLine at the custom formatter; its other fields (padding, ...) stay.
|
|
250
433
|
// A settings.json that already says exactly this is neither backed up nor written.
|
|
251
434
|
const objectOr = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
|
|
@@ -254,7 +437,7 @@ function pointStatusLine() {
|
|
|
254
437
|
const command = `node "${cmdPath}"`;
|
|
255
438
|
const prior = objectOr(settings.statusLine);
|
|
256
439
|
if (typeof prior.command === "string" && prior.command && prior.command !== command) {
|
|
257
|
-
log(`[..] statusLine ran ${
|
|
440
|
+
log(`[..] statusLine ran ${priorShown(prior.command)}; it now runs the custom HUD (the old value is in the backup)`);
|
|
258
441
|
}
|
|
259
442
|
settings.statusLine = { ...prior, type: "command", command };
|
|
260
443
|
const next = JSON.stringify(settings, null, 2) + "\n";
|
|
@@ -267,8 +450,12 @@ function pointStatusLine() {
|
|
|
267
450
|
copyFileSync(settingsPath, backup); // the original bytes and mode
|
|
268
451
|
log("[ok] Backed up settings.json -> " + backup);
|
|
269
452
|
}
|
|
270
|
-
|
|
453
|
+
const others = otherNames(settingsPath);
|
|
454
|
+
writeSettings(settingsPath, next);
|
|
271
455
|
log("[ok] statusLine -> " + command);
|
|
456
|
+
if (others > 0) {
|
|
457
|
+
log(`[..] settings.json has ${others} other name${others > 1 ? "s" : ""} (a hard link); it was written in place, so they show the new text too (the old text is in the backup)`);
|
|
458
|
+
}
|
|
272
459
|
}
|
|
273
460
|
|
|
274
461
|
// 5. Render check: run the installed formatter once with no input, as the statusLine command does,
|
|
@@ -280,7 +467,7 @@ function pointStatusLine() {
|
|
|
280
467
|
const CHECK_MS = Number(process.env.APPLY_HUD_CHECK_TIMEOUT_MS) || 25000;
|
|
281
468
|
const BLAME =
|
|
282
469
|
" OMC's install is at fault. /oh-my-claudecode:omc-setup repairs it but points statusLine back at OMC, so run setup-omc-hud again after it.";
|
|
283
|
-
const clip = (s, n = 200) =>
|
|
470
|
+
const clip = (s, n = 200) => shown(s).slice(0, n);
|
|
284
471
|
// No input, and the config folder as working folder: OMC's loader looks in the working folder's
|
|
285
472
|
// node_modules and runs git from it, and Windows looks there first for a bare program name, so the
|
|
286
473
|
// folder the installer was started in (a project, say) must not be it.
|
|
@@ -288,20 +475,31 @@ const RUN_OPTS = { input: "", encoding: "utf8", timeout: CHECK_MS, windowsHide:
|
|
|
288
475
|
|
|
289
476
|
// node as the statusLine shell finds it, but only in PATH folders given as absolute paths: spawning the
|
|
290
477
|
// bare name would on Windows try the working folder first, where a project's own node.exe could run.
|
|
291
|
-
|
|
292
|
-
|
|
478
|
+
// Like the shell, it passes over a file it may not run.
|
|
479
|
+
function findNode(pathEnv, name = process.platform === "win32" ? "node.exe" : "node") {
|
|
293
480
|
for (const entry of (pathEnv || "").split(delimiter)) {
|
|
294
481
|
const dir = entry.replace(/^"(.*)"$/, "$1");
|
|
295
482
|
if (!dir || !isAbsolute(dir)) continue;
|
|
483
|
+
const file = join(dir, name);
|
|
296
484
|
try {
|
|
297
|
-
if (statSync(
|
|
485
|
+
if (statSync(file).isFile()) {
|
|
486
|
+
accessSync(file, constants.X_OK);
|
|
487
|
+
return file;
|
|
488
|
+
}
|
|
298
489
|
} catch {
|
|
299
|
-
/* not in this folder */
|
|
490
|
+
/* not in this folder, or not runnable */
|
|
300
491
|
}
|
|
301
492
|
}
|
|
302
493
|
return null;
|
|
303
494
|
}
|
|
304
495
|
|
|
496
|
+
const NO_NODE = '[!!] Render check: no "node" on PATH; the statusLine command runs "node", so the statusline will not show.';
|
|
497
|
+
// Claude Code runs statusLine with Git Bash when it finds it, else with PowerShell. Of the two only
|
|
498
|
+
// PowerShell runs node.cmd for "node", and this check cannot tell which it will be, so it does not run.
|
|
499
|
+
const NODE_CMD_ONLY =
|
|
500
|
+
'[..] Render check: skipped; PATH has node.cmd but no node.exe, and Git Bash, which Claude Code runs statusLine with when it finds it, does not run node.cmd as "node".';
|
|
501
|
+
const noNodeLine = (pathEnv) => (process.platform === "win32" && findNode(pathEnv, "node.cmd") ? NODE_CMD_ONLY : NO_NODE);
|
|
502
|
+
|
|
305
503
|
function omcVersion(root) {
|
|
306
504
|
if (!root) return null;
|
|
307
505
|
try {
|
|
@@ -312,41 +510,58 @@ function omcVersion(root) {
|
|
|
312
510
|
}
|
|
313
511
|
}
|
|
314
512
|
|
|
513
|
+
// The first line of a Node error ("Error: ...", "TypeError [ERR_X]: ..."), "" when there is none.
|
|
514
|
+
const errorLine = (text) => /^\w*Error\b.*$/m.exec(text || "")?.[0] ?? "";
|
|
515
|
+
|
|
315
516
|
// Why an OMC version does not load: its HUD's own error when run directly.
|
|
316
517
|
function loadReason(node, root) {
|
|
317
518
|
const hud = join(root, "dist", "hud", "index.js");
|
|
318
519
|
if (!existsSync(hud)) return "no dist/hud/index.js";
|
|
319
520
|
const r = spawnSync(node, [hud], RUN_OPTS);
|
|
320
|
-
const line =
|
|
321
|
-
return line ? clip(line
|
|
521
|
+
const line = errorLine(r.stderr);
|
|
522
|
+
return line ? clip(line) : "";
|
|
322
523
|
}
|
|
323
524
|
|
|
525
|
+
const NO_OUTPUT = "[..] Render check: OMC printed nothing with no input, so this check cannot tell whether the statusline draws.";
|
|
526
|
+
|
|
324
527
|
// What the formatter printed, as report lines.
|
|
325
528
|
function judgeRender(out, node) {
|
|
326
529
|
const banner = /^\[OMC\] HUD v(\S+) \| preset: /m.exec(out);
|
|
327
530
|
if (banner) {
|
|
328
531
|
const installed = omcVersion(pluginRoot);
|
|
329
|
-
|
|
532
|
+
// Both version strings come from OMC (its output, its package.json): shown through clip like its lines.
|
|
533
|
+
const [drew, want] = [clip(banner[1], 40), clip(installed, 40)];
|
|
534
|
+
if (!installed || installed === banner[1]) return [`[ok] Render check: the statusline loads OMC ${drew}.`];
|
|
330
535
|
const why = loadReason(node, pluginRoot);
|
|
331
|
-
|
|
332
|
-
|
|
536
|
+
// Without an error from the installed version, all this check saw is which version drew.
|
|
537
|
+
if (!why) return [`[..] Render check: the statusline loads OMC ${drew}, not the installed ${want}.`];
|
|
538
|
+
return [`[!!] Render check: the statusline loads OMC ${drew}, not the installed ${want}; ${want} does not load (${why}).`, BLAME];
|
|
333
539
|
}
|
|
540
|
+
// The formatter says this when OMC printed nothing; with no input that alone does not say it cannot draw.
|
|
541
|
+
if (/^\[OMC HUD\] no output$/m.test(out)) return [NO_OUTPUT];
|
|
334
542
|
// OMC's line ends with the reason, after a path that can be long: keep it whole within reason.
|
|
335
543
|
const failed = /^\[OMC HUD\] .*$/m.exec(out);
|
|
336
544
|
if (failed) return [`[!!] Render check: OMC's HUD does not load: ${clip(failed[0], 1000)}`, BLAME];
|
|
545
|
+
// A loader that cannot load prints Node's error, header lines first; its module path can be long.
|
|
546
|
+
const error = errorLine(out);
|
|
547
|
+
if (error) return [`[!!] Render check: the statusline prints an error instead of a line: ${clip(error, 1000)}`];
|
|
337
548
|
return [`[..] Render check: unrecognized output: ${clip(out.split("\n")[0])}`];
|
|
338
549
|
}
|
|
339
550
|
|
|
551
|
+
// A statusline command that exited non-zero: its Node error line when stderr has one, else the first line.
|
|
552
|
+
function failedLine(r) {
|
|
553
|
+
const stderr = (r.stderr || "").trim();
|
|
554
|
+
const err = errorLine(stderr) || stderr.split("\n")[0];
|
|
555
|
+
return `[!!] Render check: the statusline command failed (exit ${r.status ?? r.signal})${err ? `: ${clip(err)}` : ""}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
340
558
|
function renderReport() {
|
|
341
559
|
const node = findNode(process.env.PATH);
|
|
342
|
-
if (!node) return [
|
|
560
|
+
if (!node) return [noNodeLine(process.env.PATH)];
|
|
343
561
|
const r = spawnSync(node, [wrapperDst], RUN_OPTS);
|
|
344
562
|
if (r.error && r.error.code === "ETIMEDOUT") return [`[!!] Render check: the statusline gave no answer in ${CHECK_MS / 1000}s.`];
|
|
345
563
|
if (r.error) throw r.error;
|
|
346
|
-
if (r.status !== 0)
|
|
347
|
-
const err = (r.stderr || "").trim().split("\n")[0];
|
|
348
|
-
return [`[!!] Render check: the statusline command failed (exit ${r.status ?? r.signal})${err ? `: ${clip(err)}` : ""}`];
|
|
349
|
-
}
|
|
564
|
+
if (r.status !== 0) return [failedLine(r)];
|
|
350
565
|
return judgeRender((r.stdout || "").replace(/\x1b\[[0-9;]*m/g, ""), node);
|
|
351
566
|
}
|
|
352
567
|
|
|
@@ -364,7 +579,7 @@ try {
|
|
|
364
579
|
pointStatusLine();
|
|
365
580
|
} catch (e) {
|
|
366
581
|
log(`[x] Stopped partway (${e.code || e.message}); some files may have changed.`);
|
|
367
|
-
log(` Earlier versions it replaced, apart from the loader and the
|
|
582
|
+
log(` Earlier versions it replaced, apart from the loader and blocks as the tools wrote them, are in <file>.backup.${stamp}.`);
|
|
368
583
|
process.exit(2);
|
|
369
584
|
}
|
|
370
585
|
|
|
@@ -18,8 +18,10 @@
|
|
|
18
18
|
* segment each: 🤖1|🔧14|⚡3. A count OMC leaves out (zero) stays out.
|
|
19
19
|
* - per-model usage badge (fable:0%(1h31m)) -> Fa:0%/1h31m, after the rate windows.
|
|
20
20
|
* - PL:<MB, 1 decimal>M always shown before se. It reads payload-mon's module
|
|
21
|
-
* copy
|
|
22
|
-
*
|
|
21
|
+
* copy, payload-mon/payload-size.mjs, from the first of these folders that
|
|
22
|
+
* holds it as a regular file: this file's (the one a statusLine link points
|
|
23
|
+
* at), the link's, the config folder's hud/, then the folder import.meta.url
|
|
24
|
+
* names (the link's under --preserve-symlinks); without it PL is left out.
|
|
23
25
|
* - cwd group split on "|"; branch shown without "branch:" after the account.
|
|
24
26
|
* - OMC#X.Y.Z label no longer shown, nor OMC's update hint lines
|
|
25
27
|
* ("[!] omc X - paste: ! ...", "[!] claude X - paste: ! claude update").
|
|
@@ -68,14 +70,43 @@
|
|
|
68
70
|
* We wait for the first complete line instead.
|
|
69
71
|
*/
|
|
70
72
|
import { spawnSync } from "node:child_process";
|
|
71
|
-
import { readFileSync } from "node:fs";
|
|
73
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
72
74
|
import { homedir } from "node:os";
|
|
73
|
-
import { dirname, join } from "node:path";
|
|
75
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
74
76
|
import { Readable } from "node:stream";
|
|
75
77
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
76
78
|
|
|
77
79
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
78
|
-
|
|
80
|
+
// The statusLine command names this file. When that name is a link (dotfiles, say), node runs the
|
|
81
|
+
// file it points at: the installer puts omc-hud.mjs beside the link and payload-mon puts its copy
|
|
82
|
+
// beside the file, so both folders are looked in. The command's name counts only when it leads to
|
|
83
|
+
// this file (another script may have imported this one), and the file's own folder comes from
|
|
84
|
+
// realpath: with --preserve-symlinks-main, import.meta.url names the link.
|
|
85
|
+
function hereDirs() {
|
|
86
|
+
const dirs = { named: null, real: __dirname, self: __dirname };
|
|
87
|
+
try {
|
|
88
|
+
const real = realpathSync(fileURLToPath(import.meta.url));
|
|
89
|
+
dirs.real = dirname(real);
|
|
90
|
+
const named = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
91
|
+
if (named && realpathSync(named) === real) dirs.named = dirname(named);
|
|
92
|
+
} catch {
|
|
93
|
+
/* keep this file's own folder */
|
|
94
|
+
}
|
|
95
|
+
return dirs;
|
|
96
|
+
}
|
|
97
|
+
const HERE = hereDirs();
|
|
98
|
+
const isFile = (p) => {
|
|
99
|
+
try {
|
|
100
|
+
return statSync(p).isFile();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
// The first of these folders (each once, in this order) that holds the file as a regular file, "" when none
|
|
106
|
+
// does: a folder at that name is no module, and opening a FIFO waits for a writer for good.
|
|
107
|
+
function findSibling(dirs, ...parts) {
|
|
108
|
+
return [...new Set(dirs.filter(Boolean))].map((d) => join(d, ...parts)).find(isFile) ?? "";
|
|
109
|
+
}
|
|
79
110
|
|
|
80
111
|
const STDIN_TIMEOUT_MS = 2000; // Claude Code pipes the payload immediately
|
|
81
112
|
const RENDER_TIMEOUT_MS = 10000; // see timer discipline note above
|
|
@@ -122,10 +153,39 @@ function emit(s) {
|
|
|
122
153
|
// that file in $CLAUDE_CONFIG_DIR when set, otherwise in the home folder.
|
|
123
154
|
function claudeJsonDir() {
|
|
124
155
|
const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
|
|
125
|
-
|
|
156
|
+
return v ? expandDir(v) : homedir();
|
|
157
|
+
}
|
|
158
|
+
// Where Claude Code was started (the statusline JSON's workspace.project_dir, else the working
|
|
159
|
+
// folder). A relative CLAUDE_CONFIG_DIR is read from there: Claude Code reads it from its own working
|
|
160
|
+
// folder, which is the same one until it moves (into a worktree, say).
|
|
161
|
+
function startDir() {
|
|
162
|
+
try {
|
|
163
|
+
const d = JSON.parse(input)?.workspace?.project_dir;
|
|
164
|
+
if (typeof d === "string" && isAbsolute(d)) return d;
|
|
165
|
+
} catch {
|
|
166
|
+
/* no or bad stdin JSON */
|
|
167
|
+
}
|
|
168
|
+
return process.cwd();
|
|
169
|
+
}
|
|
170
|
+
function expandDir(v) {
|
|
126
171
|
if (v === "~") return homedir();
|
|
127
172
|
if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
|
|
128
|
-
return v;
|
|
173
|
+
return resolve(startDir(), v);
|
|
174
|
+
}
|
|
175
|
+
// Claude Code's config folder. Its hud/ is where the installer puts the loader: the third place
|
|
176
|
+
// siblings are looked for, as when another script imported this file through a link. "" when the
|
|
177
|
+
// home folder cannot be told (no HOME and no account entry) or the working folder is gone: the
|
|
178
|
+
// other places still count.
|
|
179
|
+
function configDir() {
|
|
180
|
+
const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
|
|
181
|
+
return v ? expandDir(v) : join(homedir(), ".claude");
|
|
182
|
+
}
|
|
183
|
+
function configHud() {
|
|
184
|
+
try {
|
|
185
|
+
return join(configDir(), "hud");
|
|
186
|
+
} catch {
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
129
189
|
}
|
|
130
190
|
function loginAccount() {
|
|
131
191
|
try {
|
|
@@ -175,6 +235,7 @@ function readStdinBounded(ms) {
|
|
|
175
235
|
}
|
|
176
236
|
|
|
177
237
|
const input = await readStdinBounded(STDIN_TIMEOUT_MS);
|
|
238
|
+
const HUD = findSibling([HERE.named, HERE.real, configHud(), HERE.self], "omc-hud.mjs");
|
|
178
239
|
|
|
179
240
|
// Current reasoning effort level (low/medium/high/xhigh) from the statusline
|
|
180
241
|
// payload (effort.level) — shown next to the model. Empty if absent.
|
|
@@ -278,13 +339,18 @@ for (const s of [process.stdout, process.stderr]) {
|
|
|
278
339
|
}
|
|
279
340
|
|
|
280
341
|
let raw = "";
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
342
|
+
if (!HUD) {
|
|
343
|
+
// No regular file at any place looked in: nothing to run. Named where the installer puts it.
|
|
344
|
+
emit(`[OMC HUD] omc-hud.mjs not found: ${join(HERE.named || HERE.real, "omc-hud.mjs")}\n`);
|
|
345
|
+
} else {
|
|
346
|
+
try {
|
|
347
|
+
raw = (await renderInProcess(RENDER_TIMEOUT_MS)).replace(/\r/g, "");
|
|
348
|
+
} catch {
|
|
349
|
+
raw = ""; // fall through to the child-process path
|
|
350
|
+
}
|
|
285
351
|
}
|
|
286
352
|
|
|
287
|
-
if (!raw.trim()) {
|
|
353
|
+
if (HUD && !raw.trim()) {
|
|
288
354
|
// In-process rendering produced nothing usable. Fall back to the original
|
|
289
355
|
// out-of-process path: it costs an extra node, but a statusline that renders
|
|
290
356
|
// beats one that does not — e.g. if a future OMC version stops being
|
|
@@ -481,8 +547,9 @@ try {
|
|
|
481
547
|
// without it (payload-mon off) or without a transcript, PL is left out.
|
|
482
548
|
try {
|
|
483
549
|
const transcript = JSON.parse(input)?.transcript_path;
|
|
484
|
-
|
|
485
|
-
|
|
550
|
+
const copy = transcript ? findSibling([HERE.real, HERE.named, configHud(), HERE.self], "payload-mon", "payload-size.mjs") : "";
|
|
551
|
+
if (copy) {
|
|
552
|
+
const { estimatePayloadBytes, REQUEST_LIMIT_BYTES } = await import(pathToFileURL(copy).href);
|
|
486
553
|
const bytes = estimatePayloadBytes(transcript);
|
|
487
554
|
if (Number.isFinite(bytes)) {
|
|
488
555
|
for (let i = colored.length - 1; i >= 0; i--) if (/^payload:/.test(stripAnsi(colored[i]))) colored.splice(i, 1);
|
|
@@ -35,7 +35,7 @@ const OPEN = "// >>> claude-update-last >>>";
|
|
|
35
35
|
const CLOSE = "// <<< claude-update-last <<<";
|
|
36
36
|
// A block's body never crosses another opening marker, so a stray one above a
|
|
37
37
|
// block is left over (and refused) instead of taking the code between with it.
|
|
38
|
-
const BLOCKS =
|
|
38
|
+
export const BLOCKS =
|
|
39
39
|
/^[ \t]*\/\/ >>> claude-update-last >>>(?:(?!\/\/ >>> claude-update-last >>>)[\s\S])*?\/\/ <<< claude-update-last <<<[ \t]*(?:\r?\n|(?![\s\S]))/gm;
|
|
40
40
|
// The block goes right before the line that joins the segments into the statusline.
|
|
41
41
|
const JOIN_LINE = /^ {2}let result = colored\.join\(SEP\);\r?$/gm;
|
|
@@ -150,12 +150,14 @@ function writeFresh(file, data) {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
// The first lines of node's complaint about `text` as an ES module, "" if it parses.
|
|
153
|
+
// The first lines of node's complaint about `text` as an ES module, "" if it parses. A leading BOM
|
|
154
|
+
// is left out: node runs a wrapper that starts with one, but `node --check` rejects a BOM before a
|
|
155
|
+
// shebang (Node 20, 24).
|
|
154
156
|
function syntaxError(text) {
|
|
155
157
|
const dir = mkdtempSync(join(tmpdir(), "claude-update-last-"));
|
|
156
158
|
try {
|
|
157
159
|
const file = join(dir, "check.mjs");
|
|
158
|
-
writeFileSync(file, text);
|
|
160
|
+
writeFileSync(file, text.replace(/^\uFEFF/, ""));
|
|
159
161
|
const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
160
162
|
return r.status === 0 ? "" : String(r.stderr || r.error).trim().split("\n").slice(0, 3).join(" | ");
|
|
161
163
|
} finally {
|