@kaydash9999/banker-plugins 0.13.0 → 0.15.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.
Files changed (82) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +82 -0
  4. package/README.md +50 -17
  5. package/codex/manifest.json +11 -8
  6. package/codex/transform-matrix.md +16 -4
  7. package/commands/front-qa.md +8 -5
  8. package/commands/setup.md +13 -4
  9. package/hooks/graceful-pause.mjs +222 -0
  10. package/hooks/hooks.json +2 -1
  11. package/hooks/register.mjs +12 -0
  12. package/package.json +4 -1
  13. package/skills/3d-intro-build/SKILL.md +1 -1
  14. package/skills/3d-intro-setup/SKILL.md +1 -1
  15. package/skills/all-in-one/SKILL.md +160 -55
  16. package/skills/append_wiki/SKILL.md +1 -1
  17. package/skills/arch-diagram/SKILL.md +1 -1
  18. package/skills/audit-mock/SKILL.md +1 -1
  19. package/skills/audit-security/SKILL.md +1 -1
  20. package/skills/audit-web-page/SKILL.md +1 -1
  21. package/skills/cleansing-memory/SKILL.md +1 -1
  22. package/skills/compact-copy/SKILL.md +14 -13
  23. package/skills/compact-wiki/SKILL.md +1 -1
  24. package/skills/curation/SKILL.md +36 -3
  25. package/skills/deep-init/SKILL.md +1 -1
  26. package/skills/deep-research/SKILL.md +1 -1
  27. package/skills/docs-setup/SKILL.md +1 -1
  28. package/skills/humanizer/SKILL.md +1 -1
  29. package/skills/interval-report/SKILL.md +1 -1
  30. package/skills/lineage/SKILL.md +146 -16
  31. package/skills/lineage/lineage.py +1091 -107
  32. package/skills/make-notion-guide/SKILL.md +1 -1
  33. package/skills/motion-graphic-make/SKILL.md +1 -1
  34. package/skills/motion-graphic-setup/SKILL.md +1 -1
  35. package/skills/nothing-design/SKILL.md +1 -1
  36. package/skills/obsidizer/SKILL.md +1 -1
  37. package/skills/omc-reference/SKILL.md +68 -33
  38. package/skills/payload-mon/SKILL.md +66 -0
  39. package/skills/payload-mon/scripts/payload-mon.mjs +377 -0
  40. package/skills/payload-mon/scripts/payload-size.mjs +238 -0
  41. package/skills/pdf-vision-extract/SKILL.md +1 -1
  42. package/skills/play-qa/SKILL.md +1 -1
  43. package/skills/ralph-qa/SKILL.md +263 -112
  44. package/skills/ralph-qa/references/gemini-read-only.toml +9 -0
  45. package/skills/ralph-qa/references/gemini-seat.mjs +175 -0
  46. package/skills/ralph-qa/references/payload-scan.mjs +130 -0
  47. package/skills/ralph-qa/references/verifier-probe.mjs +872 -780
  48. package/skills/ready-compact/SKILL.md +114 -67
  49. package/skills/refresh-git-ignore/SKILL.md +1 -1
  50. package/skills/refresh-readme/SKILL.md +3 -3
  51. package/skills/rfp-author/SKILL.md +3 -3
  52. package/skills/setup-bypass-permissions/SKILL.md +123 -0
  53. package/skills/setup-bypass-permissions/agents/openai.yaml +4 -0
  54. package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +381 -0
  55. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.ps1 +247 -0
  56. package/skills/setup-bypass-permissions/scripts/fallback/bypass-permissions.py +241 -0
  57. package/skills/{harness-factory → setup-harness-factory}/SKILL.md +3 -3
  58. package/skills/setup-insane-search/SKILL.md +1 -1
  59. package/skills/setup-java/SKILL.md +1 -1
  60. package/skills/setup-lsp/SKILL.md +1 -1
  61. package/skills/setup-mcp/SKILL.md +1 -1
  62. package/skills/setup-node/SKILL.md +1 -1
  63. package/skills/setup-omc/SKILL.md +11 -4
  64. package/skills/setup-omc-hud/SKILL.md +26 -1
  65. package/skills/setup-omc-hud/scripts/claude-update-last.mjs +247 -0
  66. package/skills/setup-playwright/SKILL.md +1 -1
  67. package/skills/setup-pwsh/SKILL.md +1 -1
  68. package/skills/setup-python/SKILL.md +1 -1
  69. package/skills/setup-sandbox/SKILL.md +1 -1
  70. package/skills/setup-stitch/SKILL.md +1 -1
  71. package/skills/setup-tmux/SKILL.md +1 -1
  72. package/skills/smart-compact/SKILL.md +10 -9
  73. package/skills/summary-wiki/SKILL.md +1 -1
  74. package/skills/tone-compact/SKILL.md +203 -0
  75. package/skills/tone-compact/scripts/tone-compact.mjs +499 -0
  76. package/skills/ultra-init/SKILL.md +53 -42
  77. package/skills/ultra-interview/SKILL.md +1 -1
  78. package/skills/ultra-ui-qa/SKILL.md +1 -1
  79. package/skills/update-banker/SKILL.md +1 -1
  80. package/skills/vertical-pptx/SKILL.md +1 -1
  81. package/skills/vertical-pptx-setup/SKILL.md +1 -1
  82. package/skills/visual-ralph/SKILL.md +1 -1
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * payload-mon: turns the statusline payload segment on or off.
4
+ *
5
+ * node payload-mon.mjs on add the payload-mon block to the OMC HUD wrapper
6
+ * node payload-mon.mjs off take it out again, with the module copy and the cache
7
+ * node payload-mon.mjs status say whether it is on, with this session's estimate
8
+ *
9
+ * The wrapper (<config>/hud/omc-hud-custom.mjs, where <config> is
10
+ * $CLAUDE_CONFIG_DIR or ~/.claude; $PAYLOAD_MON_HUD_FILE overrides the path)
11
+ * gets one marker-delimited block right after the line that adds ctx, which
12
+ * works out the segment and places it there. Blocks other tools add to the same
13
+ * wrapper with their own markers are left alone.
14
+ *
15
+ * Two wrapper layouts take the block. The older one reads stdin into `let input`
16
+ * and runs the OMC HUD through spawnSync; the current omc_hud one reads it into
17
+ * `const input`, runs the HUD in-process and keeps spawnSync for a fallback
18
+ * branch. A block anchored on spawnSync would land in that branch, so the ctx
19
+ * line is the only anchor. Earlier versions that also put a block before
20
+ * spawnSync are converted on the next `on`.
21
+ *
22
+ * The block imports a copy of payload-size.mjs kept beside the wrapper
23
+ * (<hud>/payload-mon/payload-size.mjs), not the module in this folder: a plugin
24
+ * install lives under a versioned path that the next update retires, and an
25
+ * import pointing there would go dark without a word. `on` writes the copy and
26
+ * refreshes it when this version's module differs; `status` says when it does.
27
+ *
28
+ * `on` converges on the current block and is a no-op when it is in place. It
29
+ * writes only into the OMC custom HUD (signature-gated); `off` removes only
30
+ * payload-mon's own marker blocks. Both refuse markers that do not pair up, keep
31
+ * the text from before each change in <wrapper>.payload-mon.bak, and write
32
+ * nothing that fails `node --check`.
33
+ * Exit status 1 means nothing was changed; 2 means an unexpected error stopped
34
+ * the command partway, so check `status`.
35
+ */
36
+ import { spawnSync } from "node:child_process";
37
+ import {
38
+ accessSync,
39
+ constants,
40
+ existsSync,
41
+ mkdirSync,
42
+ mkdtempSync,
43
+ readdirSync,
44
+ readFileSync,
45
+ realpathSync,
46
+ renameSync,
47
+ rmdirSync,
48
+ rmSync,
49
+ writeFileSync,
50
+ } from "node:fs";
51
+ import { homedir, tmpdir } from "node:os";
52
+ import { basename, dirname, join } from "node:path";
53
+ import { fileURLToPath, pathToFileURL } from "node:url";
54
+ import { CACHE_DIR, clearCache, estimatePayloadBytes, REQUEST_LIMIT_BYTES, SHOW_FROM_BYTES } from "./payload-size.mjs";
55
+
56
+ const OPEN = "// >>> payload-mon >>>";
57
+ const CLOSE = "// <<< payload-mon <<<";
58
+ const BLOCKS = /^[ \t]*\/\/ >>> payload-mon >>>[\s\S]*?\/\/ <<< payload-mon <<<[ \t]*\r?\n/gm;
59
+ // The block goes right after the line that adds ctx.
60
+ const CTX_LINE = /^ {2}if \(f\.ctx\) colored\.push\(colorCtx\(f\.ctx\)\);\r?\n/m;
61
+ // The statusline JSON the block reads, in either layout.
62
+ const INPUT_DECL = /^(?:let|const) input\b/m;
63
+ const MB = 1024 * 1024;
64
+
65
+ const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
66
+
67
+ const DEFAULTS = {
68
+ hudFile: process.env.PAYLOAD_MON_HUD_FILE || join(CONFIG_DIR, "hud", "omc-hud-custom.mjs"),
69
+ moduleSource: fileURLToPath(new URL("./payload-size.mjs", import.meta.url)),
70
+ cacheDir: CACHE_DIR,
71
+ settingsFile: join(CONFIG_DIR, "settings.json"),
72
+ projectsDir: join(CONFIG_DIR, "projects"),
73
+ sessionId: process.env.CLAUDE_CODE_SESSION_ID,
74
+ };
75
+
76
+ // Where the block's module lives: beside the wrapper, outside any versioned path.
77
+ const moduleCopy = (hudFile) => join(dirname(hudFile), "payload-mon", "payload-size.mjs");
78
+ const moduleUrl = (hudFile) => pathToFileURL(moduleCopy(hudFile)).href;
79
+
80
+ // Right after ctx, colored like the 5h/wk rates: % on the ctx scale, MB in cyan.
81
+ // Its names are block-scoped, so none can clash with the wrapper's own.
82
+ const placeBlock = (url) =>
83
+ [
84
+ " " + OPEN,
85
+ " // payload-mon: estimated API request payload against the 32MB request limit, shown",
86
+ " // like ctx from 8MB (25%) on. `payload-mon off` removes this block; a missing or",
87
+ " // broken module costs only this segment, never the statusline.",
88
+ " try {",
89
+ " const transcript = JSON.parse(input)?.transcript_path;",
90
+ " if (transcript) {",
91
+ ` const { estimatePayloadBytes, payloadSegment } = await import(${JSON.stringify(url)});`,
92
+ " const segment = payloadSegment(estimatePayloadBytes(transcript));",
93
+ " if (segment) colored.push(colorRate(segment));",
94
+ " }",
95
+ " } catch {",
96
+ " /* no stdin JSON, unreadable transcript or module error */",
97
+ " }",
98
+ " " + CLOSE,
99
+ "",
100
+ ].join("\n");
101
+
102
+ const insertBlock = (base, url) => base.replace(CTX_LINE, (line) => line + placeBlock(url));
103
+
104
+ const count = (text, needle) => text.split(needle).length - 1;
105
+
106
+ // The wrapper's text, null when there is no such file. Any other read failure
107
+ // (permissions, say) is not "missing" and propagates.
108
+ const readText = (file) => {
109
+ try {
110
+ return readFileSync(file, "utf8");
111
+ } catch (e) {
112
+ if (e.code === "ENOENT" || e.code === "ENOTDIR") return null;
113
+ throw e;
114
+ }
115
+ };
116
+
117
+ const readBytes = (file) => {
118
+ try {
119
+ return readFileSync(file);
120
+ } catch {
121
+ return null;
122
+ }
123
+ };
124
+
125
+ const refusal = (why, hudFile) => ({ ok: false, message: `${why}. 래퍼를 그대로 두었습니다: ${hudFile}` });
126
+
127
+ // The block exactly as `on` writes it for this URL, once, right after the ctx line.
128
+ function inPlace(src, url) {
129
+ if (count(src, OPEN) !== 1 || count(src, CLOSE) !== 1) return false;
130
+ const ctx = CTX_LINE.exec(src);
131
+ return ctx !== null && src.startsWith(placeBlock(url), ctx.index + ctx[0].length);
132
+ }
133
+
134
+ // Why the block cannot go into this wrapper, or "" when it can. Markers that do
135
+ // not pair up would let the block pattern swallow the code between them.
136
+ function misfit(src) {
137
+ if (count(src, OPEN) !== count(src, CLOSE)) return "payload-mon 표시(>>> / <<<)의 짝이 맞지 않습니다";
138
+ const base = src.replace(BLOCKS, "");
139
+ if (!base.includes("omc-hud.mjs") || !base.includes("const colorRate")) return "OMC 커스텀 HUD 래퍼가 아닙니다";
140
+ if (!INPUT_DECL.test(base)) return "삽입 위치를 찾지 못했습니다(statusline 입력을 담는 input 변수가 없음)";
141
+ if (!CTX_LINE.test(base)) return "삽입 위치를 찾지 못했습니다(ctx를 넣는 줄이 없음)";
142
+ return "";
143
+ }
144
+
145
+ // "current", "stale" (differs from this version's module) or "missing".
146
+ function copyState(o) {
147
+ const copy = readBytes(moduleCopy(o.hudFile));
148
+ if (copy === null) return "missing";
149
+ const source = readBytes(o.moduleSource);
150
+ return source !== null && copy.equals(source) ? "current" : "stale";
151
+ }
152
+
153
+ // Replaces file through a fresh temp file ("wx") and a rename: a link planted at
154
+ // either name is replaced rather than written through, and a write that fails
155
+ // leaves the old file as it was.
156
+ function writeFresh(file, data, mode) {
157
+ const tmp = `${file}.${process.pid}.tmp`;
158
+ rmSync(tmp, { force: true });
159
+ try {
160
+ writeFileSync(tmp, data, { flag: "wx", mode });
161
+ renameSync(tmp, file);
162
+ } catch (e) {
163
+ rmSync(tmp, { force: true });
164
+ throw e;
165
+ }
166
+ }
167
+
168
+ // Brings the copy up to this version's module; true when it had to write. The
169
+ // statusline imports it on every refresh, so no other account may change it
170
+ // (reading it is harmless). When the write fails, a folder made here is taken
171
+ // away again: mkdirSync names a folder only when it created it, and rmdir
172
+ // leaves one that something else has meanwhile put a file in.
173
+ function syncModule(o) {
174
+ if (copyState(o) === "current") return false;
175
+ const copy = moduleCopy(o.hudFile);
176
+ const made = mkdirSync(dirname(copy), { recursive: true, mode: 0o755 }) !== undefined;
177
+ try {
178
+ writeFresh(copy, readFileSync(o.moduleSource), 0o644);
179
+ } catch (e) {
180
+ if (made) {
181
+ try {
182
+ rmdirSync(dirname(copy));
183
+ } catch {
184
+ /* no longer empty, so not only ours */
185
+ }
186
+ }
187
+ throw e;
188
+ }
189
+ return true;
190
+ }
191
+
192
+ // The first lines of node's complaint about `text` as an ES module, "" if it parses.
193
+ function syntaxError(text) {
194
+ const dir = mkdtempSync(join(tmpdir(), "payload-mon-"));
195
+ try {
196
+ const file = join(dir, "check.mjs");
197
+ writeFileSync(file, text);
198
+ const r = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
199
+ return r.status === 0 ? "" : String(r.stderr || r.error).trim().split("\n").slice(0, 3).join(" | ");
200
+ } finally {
201
+ rmSync(dir, { recursive: true, force: true });
202
+ }
203
+ }
204
+
205
+ // Writes `after` over the wrapper if it parses, keeping `before` in the .bak.
206
+ // `prepare` runs once the text is known to parse and the wrapper to be writable,
207
+ // before the wrapper is touched, so a failure there leaves the wrapper as it was.
208
+ function commit(hudFile, before, after, done, prepare = () => {}) {
209
+ const err = syntaxError(after);
210
+ if (err) return { ok: false, message: `node --check 실패로 래퍼를 그대로 두었습니다: ${err}` };
211
+ try {
212
+ accessSync(hudFile, constants.W_OK);
213
+ } catch (e) {
214
+ return refusal(`래퍼에 쓸 권한이 없습니다(${e.code || e.message})`, hudFile);
215
+ }
216
+ try {
217
+ prepare();
218
+ } catch (e) {
219
+ return refusal(`모듈 사본을 쓰지 못했습니다(${e.code || e.message})`, hudFile);
220
+ }
221
+ writeFresh(`${hudFile}.payload-mon.bak`, before);
222
+ writeFileSync(hudFile, after);
223
+ return { ok: true, message: done };
224
+ }
225
+
226
+ // Whether settings.json runs this wrapper as the statusline.
227
+ function statusLineUses(settingsFile, hudFile) {
228
+ try {
229
+ const command = JSON.parse(readFileSync(settingsFile, "utf8"))?.statusLine?.command;
230
+ return typeof command === "string" && command.includes(basename(hudFile));
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ const notShownNote = (o) =>
237
+ statusLineUses(o.settingsFile, o.hudFile)
238
+ ? ""
239
+ : `\n주의: statusLine 명령에 ${basename(o.hudFile)}가 보이지 않습니다. ` +
240
+ "다른 래퍼(smart-compact 등)가 HUD를 감싸 호출하면 정상 표시되고, 아니면 화면에 나타나지 않습니다.";
241
+
242
+ export function turnOn(opts = {}) {
243
+ const o = { ...DEFAULTS, ...opts };
244
+ const src = readText(o.hudFile);
245
+ if (src === null) return { ok: false, message: `HUD 래퍼가 없습니다: ${o.hudFile}` };
246
+ if (inPlace(src, moduleUrl(o.hudFile))) {
247
+ try {
248
+ const refreshed = syncModule(o);
249
+ return { ok: true, message: (refreshed ? "모듈 사본을 이 버전으로 맞췄습니다." : "이미 켜져 있습니다 (변경 없음).") + notShownNote(o) };
250
+ } catch (e) {
251
+ return { ok: false, message: `모듈 사본을 쓰지 못했습니다(${e.code || e.message}): ${moduleCopy(o.hudFile)}` };
252
+ }
253
+ }
254
+ const why = misfit(src);
255
+ if (why) return refusal(why, o.hudFile);
256
+ const done = "켰습니다. 다음 statusline 갱신부터 8MB 이상이면 ctx 옆에 payload가 표시됩니다.";
257
+ const after = insertBlock(src.replace(BLOCKS, ""), moduleUrl(o.hudFile));
258
+ return commit(o.hudFile, src, after, done + notShownNote(o), () => syncModule(o));
259
+ }
260
+
261
+ function removeBlocks(o, src) {
262
+ if (src === null) return { ok: true, message: `HUD 래퍼가 없어 끌 것이 없습니다: ${o.hudFile}` };
263
+ if (count(src, OPEN) !== count(src, CLOSE)) return refusal("payload-mon 표시(>>> / <<<)의 짝이 맞지 않습니다", o.hudFile);
264
+ const out = src.replace(BLOCKS, "");
265
+ if (out === src) return { ok: true, message: "이미 꺼져 있습니다 (래퍼 변경 없음)." };
266
+ return commit(o.hudFile, src, out, "껐습니다. 다음 statusline 갱신부터 payload가 표시되지 않습니다.");
267
+ }
268
+
269
+ // What off leaves behind otherwise: the cache (when this account controls its
270
+ // folder), and the module copy with its folder (only when nothing else is in
271
+ // it). Neither failing undoes off.
272
+ function removeLeftovers(o) {
273
+ const copy = moduleCopy(o.hudFile);
274
+ for (const remove of [() => clearCache(o.cacheDir), () => rmSync(copy, { force: true })]) {
275
+ try {
276
+ remove();
277
+ } catch {
278
+ /* stale numbers or an unused copy; off stands */
279
+ }
280
+ }
281
+ try {
282
+ rmdirSync(dirname(copy));
283
+ } catch {
284
+ /* absent, or holds something that is not ours */
285
+ }
286
+ }
287
+
288
+ export function turnOff(opts = {}) {
289
+ const o = { ...DEFAULTS, ...opts };
290
+ const res = removeBlocks(o, readText(o.hudFile));
291
+ if (res.ok) removeLeftovers(o);
292
+ return res;
293
+ }
294
+
295
+ // "현재 세션: 0.8MB / 32MB (2%), 8MB부터 표시" for the session that runs this, "" if unknown.
296
+ function sessionEstimate(o) {
297
+ if (!o.sessionId || !existsSync(o.projectsDir)) return "";
298
+ const file = `${o.sessionId}.jsonl`;
299
+ const project = readdirSync(o.projectsDir).find((d) => existsSync(join(o.projectsDir, d, file)));
300
+ if (!project) return "";
301
+ try {
302
+ const bytes = estimatePayloadBytes(join(o.projectsDir, project, file), o.cacheDir);
303
+ const pct = Math.floor((bytes / REQUEST_LIMIT_BYTES) * 100);
304
+ return `현재 세션: ${(bytes / MB).toFixed(1)}MB / 32MB (${pct}%), ${SHOW_FROM_BYTES / MB}MB부터 표시`;
305
+ } catch {
306
+ return "";
307
+ }
308
+ }
309
+
310
+ const COPY_NOTE = {
311
+ current: "",
312
+ stale: ", 단 모듈 사본이 이 버전과 다름 (on 으로 갱신)",
313
+ missing: ", 단 모듈 사본이 없어 표시되지 않음 (on 으로 복구)",
314
+ };
315
+
316
+ function stateOf(src, o) {
317
+ if (src === null) return "HUD 래퍼 없음";
318
+ if (count(src, OPEN) + count(src, CLOSE) === 0) return "꺼짐";
319
+ // on and off both refuse this, so pointing at on would be a dead end.
320
+ if (count(src, OPEN) !== count(src, CLOSE)) return "표시(>>> / <<<)의 짝이 맞지 않습니다 (on·off 모두 거부: HUD 재설치 또는 남은 표시 줄 정리)";
321
+ if (!inPlace(src, moduleUrl(o.hudFile))) return "켜짐이 불완전하거나 오래됨 (on 으로 복구)";
322
+ return "켜짐" + COPY_NOTE[copyState(o)];
323
+ }
324
+
325
+ // What status says about the wrapper. A wrapper it cannot read is reported, not
326
+ // thrown: status is where an exit 2 sends the user to look.
327
+ function describe(o) {
328
+ try {
329
+ return stateOf(readText(o.hudFile), o);
330
+ } catch (e) {
331
+ return `HUD 래퍼를 읽을 수 없음 (${e.code || e.message})`;
332
+ }
333
+ }
334
+
335
+ export function status(opts = {}) {
336
+ const o = { ...DEFAULTS, ...opts };
337
+ const lines = [`payload-mon: ${describe(o)} (${o.hudFile})`];
338
+ const note = notShownNote(o);
339
+ if (note) lines.push(note.trim());
340
+ const estimate = sessionEstimate(o);
341
+ if (estimate) lines.push(estimate);
342
+ return { ok: true, message: lines.join("\n") };
343
+ }
344
+
345
+ const COMMANDS = new Map([
346
+ ["on", turnOn],
347
+ ["off", turnOff],
348
+ ["status", status],
349
+ ]);
350
+
351
+ function main(arg) {
352
+ const run = COMMANDS.get(arg);
353
+ if (!run) {
354
+ process.stdout.write("사용법: payload-mon on | off | status\n");
355
+ return 1;
356
+ }
357
+ try {
358
+ const { ok, message } = run();
359
+ process.stdout.write(message + "\n");
360
+ return ok ? 0 : 1;
361
+ } catch (e) {
362
+ process.stdout.write(`예기치 못한 오류로 중단했습니다: ${e.message}\nstatus 로 상태를 확인하세요.\n`);
363
+ return 2;
364
+ }
365
+ }
366
+
367
+ // Run as a command, also when reached through a symlinked path: Node resolves
368
+ // symlinks for import.meta.url but not for process.argv[1].
369
+ const invokedDirectly = () => {
370
+ try {
371
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
372
+ } catch {
373
+ return false;
374
+ }
375
+ };
376
+
377
+ if (invokedDirectly()) process.exitCode = main((process.argv[2] || "status").toLowerCase());
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Estimated API request payload of a Claude Code session, for the HUD.
3
+ *
4
+ * Claude Code refuses a request over the API's 32MB limit ("Request too large
5
+ * (max 32MB)") and measures it as the JSON size of the messages it sends
6
+ * ("request_body_over_limit: body=…B (messages only)"). This applies the same
7
+ * measure to the session transcript: everything Claude Code would send since the
8
+ * last compact boundary (images and tool results included), picked with the same
9
+ * filter it uses to build a request (see sentPart).
10
+ *
11
+ * Known gaps, all small next to the 8MB threshold in the sessions measured:
12
+ * - the system prompt and tool schemas are not in the transcript, so the real
13
+ * request is a few hundred KB larger;
14
+ * - after a rewind (Esc Esc) the abandoned branch stays counted until the next
15
+ * compaction;
16
+ * - the few messages a compaction keeps verbatim before its boundary are not
17
+ * added back (13KB or less in each of the 53 compactions measured);
18
+ * - tool results Claude Code clears at request time (microcompact, not seen in
19
+ * these transcripts) stay counted.
20
+ *
21
+ * The transcript only grows, so the scanned offset and running total are cached
22
+ * per transcript under $TMPDIR/omc-hud-payload-<uid>/ and each render reads only
23
+ * what was appended since the last one. A first render (or a rewritten
24
+ * transcript) skims for the last compact boundary and parses only what follows it.
25
+ *
26
+ * On Linux $TMPDIR is usually the shared /tmp, where any account can create a
27
+ * folder first. The uid in the folder name keeps accounts apart, and the cache is
28
+ * used only while that folder is a real directory this account owns and no other
29
+ * account can write to; otherwise every render rescans. macOS and Windows already
30
+ * give each account its own temp folder.
31
+ *
32
+ * When a Claude Code update changes the transcript format or the request filter,
33
+ * fix sentPart and bump CACHE_VERSION, so totals cached under the old rules are
34
+ * rebuilt.
35
+ */
36
+ import { createHash } from "node:crypto";
37
+ import {
38
+ closeSync,
39
+ fstatSync,
40
+ lstatSync,
41
+ mkdirSync,
42
+ openSync,
43
+ readFileSync,
44
+ readSync,
45
+ renameSync,
46
+ rmSync,
47
+ writeFileSync,
48
+ } from "node:fs";
49
+ import { tmpdir } from "node:os";
50
+ import { dirname, join } from "node:path";
51
+
52
+ export const REQUEST_LIMIT_BYTES = 32 * 1024 * 1024;
53
+ // Shown from 25% of the limit (8MB) on, as an early warning like ctx.
54
+ export const SHOW_FROM_BYTES = REQUEST_LIMIT_BYTES / 4;
55
+
56
+ const MB = 1024 * 1024;
57
+ // The transcript is read in pieces of this size (exported for the tests).
58
+ export const CHUNK_BYTES = 8 * MB;
59
+ const NL = 0x0a;
60
+ const BOUNDARY_MARK = Buffer.from('"compact_boundary"');
61
+ // This account's uid, or null where there is none (Windows).
62
+ const UID = typeof process.getuid === "function" ? process.getuid() : null;
63
+ // Per-transcript estimate cache, one folder per account; payload-mon off deletes it
64
+ // when this account controls the folder (clearCache).
65
+ export const CACHE_DIR = join(tmpdir(), UID === null ? "omc-hud-payload" : `omc-hud-payload-${UID}`);
66
+ // Bump whenever sentPart changes, so cached totals from older rules are rebuilt.
67
+ const CACHE_VERSION = 2;
68
+ // The model Claude Code stamps on the API-error rows it shows but never sends.
69
+ const SYNTHETIC_MODEL = "<synthetic>";
70
+
71
+ const parseLine = (text) => {
72
+ try {
73
+ return JSON.parse(text);
74
+ } catch {
75
+ return null; // torn or foreign line: nothing to count
76
+ }
77
+ };
78
+
79
+ const isBoundary = (e) => e?.type === "system" && e.subtype === "compact_boundary";
80
+
81
+ // Rows Claude Code drops from a request: virtual rows and API-error stand-ins.
82
+ const isDropped = (e) => e.isVirtual === true || (e.isApiErrorMessage === true && e.message?.model === SYNTHETIC_MODEL);
83
+
84
+ // An attachment sends its rendered text (system reminders, CLAUDE.md, file
85
+ // mentions). An image @-mention is stored without one, since Claude Code renders
86
+ // it again for each request, so its base64 stands in. A PDF mention sends only a
87
+ // short note, and hook results and snapshots send nothing.
88
+ function attachmentPart(e) {
89
+ if (e.rendered !== undefined) return e.rendered;
90
+ const { type, content } = e.attachment ?? {};
91
+ return type === "file" && content?.type === "image" ? content.file?.base64 : undefined;
92
+ }
93
+
94
+ // The part of an entry that goes into the request, mirroring Claude Code's own
95
+ // filter: user/assistant content (the compact summary included), local command
96
+ // output and attachments. Sidechains are subagents' own loops.
97
+ function sentPart(e) {
98
+ if (!e || e.isSidechain) return undefined;
99
+ if (e.type === "user" || e.type === "assistant") return isDropped(e) ? undefined : e.message?.content;
100
+ if (e.type === "system") return e.subtype === "local_command" ? e.content : undefined;
101
+ return e.type === "attachment" ? attachmentPart(e) : undefined;
102
+ }
103
+
104
+ const entryBytes = (e) => {
105
+ const sent = sentPart(e);
106
+ return sent == null ? 0 : Buffer.byteLength(JSON.stringify(sent));
107
+ };
108
+
109
+ // Reads [from, size) in chunks and hands each run of complete lines to
110
+ // onLines(buf, base): buf ends in "\n" and starts at file offset base. A last
111
+ // line still being written is left for the next render. Returns the offset just
112
+ // past the last complete line.
113
+ function readCompleteLines(fd, from, size, onLines) {
114
+ let pos = from;
115
+ let base = from;
116
+ let carry = Buffer.alloc(0);
117
+ while (pos < size) {
118
+ const chunk = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, size - pos));
119
+ const n = readSync(fd, chunk, 0, chunk.length, pos);
120
+ if (n === 0) break;
121
+ pos += n;
122
+ const data = carry.length ? Buffer.concat([carry, chunk.subarray(0, n)]) : chunk.subarray(0, n);
123
+ const end = data.lastIndexOf(NL) + 1;
124
+ if (end > 0) onLines(data.subarray(0, end), base);
125
+ base += end;
126
+ carry = data.subarray(end);
127
+ }
128
+ return base;
129
+ }
130
+
131
+ // Offset just past the last real compact boundary, 0 when there is none. Only
132
+ // lines holding the marker are parsed, so a 70MB transcript is skimmed in ~0.2s.
133
+ // Exported so the tests can pin where the count starts.
134
+ export function lastBoundaryEnd(fd, size) {
135
+ let found = 0;
136
+ readCompleteLines(fd, 0, size, (buf, base) => {
137
+ for (let k = buf.indexOf(BOUNDARY_MARK); k !== -1; ) {
138
+ const end = buf.indexOf(NL, k);
139
+ if (isBoundary(parseLine(buf.toString("utf8", buf.lastIndexOf(NL, k) + 1, end)))) found = base + end + 1;
140
+ k = buf.indexOf(BOUNDARY_MARK, end);
141
+ }
142
+ });
143
+ return found;
144
+ }
145
+
146
+ // Running total over the complete lines in [from, size). A compact boundary
147
+ // starts it over: compaction replaces everything sent before it.
148
+ function sumEntries(fd, from, size, bytes) {
149
+ let total = bytes;
150
+ const offset = readCompleteLines(fd, from, size, (buf) => {
151
+ for (let start = 0, end; (end = buf.indexOf(NL, start)) !== -1; start = end + 1) {
152
+ const e = parseLine(buf.toString("utf8", start, end));
153
+ total = isBoundary(e) ? 0 : total + entryBytes(e);
154
+ }
155
+ });
156
+ return { offset, bytes: total };
157
+ }
158
+
159
+ const cacheFile = (dir, transcriptPath) =>
160
+ join(dir, createHash("sha1").update(transcriptPath).digest("hex").slice(0, 16) + ".json");
161
+
162
+ // Whether dir is a real folder (not a link) that this account owns and no other
163
+ // account can write to. Windows has no uid and gives each account its own temp.
164
+ function ownedDir(dir) {
165
+ try {
166
+ const st = lstatSync(dir);
167
+ return st.isDirectory() && (UID === null || (st.uid === UID && (st.mode & 0o022) === 0));
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+
173
+ const loadCache = (file) => {
174
+ if (!ownedDir(dirname(file))) return null;
175
+ try {
176
+ return JSON.parse(readFileSync(file, "utf8"));
177
+ } catch {
178
+ return null;
179
+ }
180
+ };
181
+
182
+ // Write then rename, so a render running at the same time never reads half a
183
+ // file. The temp file is created fresh ("wx"), never written through whatever
184
+ // already sits at its name.
185
+ function saveCache(file, state) {
186
+ try {
187
+ const dir = dirname(file);
188
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
189
+ if (!ownedDir(dir)) return; // a folder this account does not control: no cache
190
+ const tmp = `${file}.${process.pid}.tmp`;
191
+ rmSync(tmp, { force: true });
192
+ writeFileSync(tmp, JSON.stringify(state), { flag: "wx", mode: 0o600 });
193
+ renameSync(tmp, file);
194
+ } catch {
195
+ /* the cache only saves time; the estimate stands without it */
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Deletes the estimate cache at dir, but only a folder this account controls:
201
+ * one another account created first is left alone, as it was never used.
202
+ */
203
+ export function clearCache(dir = CACHE_DIR) {
204
+ if (ownedDir(dir)) rmSync(dir, { recursive: true, force: true });
205
+ }
206
+
207
+ /**
208
+ * Estimated request payload, in bytes, of the session whose transcript is at
209
+ * transcriptPath. Throws when the transcript cannot be read.
210
+ */
211
+ export function estimatePayloadBytes(transcriptPath, cacheDir = CACHE_DIR) {
212
+ const fd = openSync(transcriptPath, "r");
213
+ try {
214
+ const { size, ino } = fstatSync(fd);
215
+ const file = cacheFile(cacheDir, transcriptPath);
216
+ const c = loadCache(file);
217
+ // Resume only a cache of these rules, for this file, which has not shrunk since.
218
+ const resume = c?.v === CACHE_VERSION && c.path === transcriptPath && c.ino === ino && c.offset <= size;
219
+ const state = resume
220
+ ? c
221
+ : { v: CACHE_VERSION, path: transcriptPath, ino, offset: lastBoundaryEnd(fd, size), bytes: 0 };
222
+ const next = { ...state, ...sumEntries(fd, state.offset, size, state.bytes) };
223
+ if (!resume || next.offset !== state.offset) saveCache(file, next);
224
+ return next.bytes;
225
+ } finally {
226
+ closeSync(fd);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * The HUD segment, "payload:28%/9.0MB" (share of the 32MB limit / size), in the
232
+ * 5h/wk layout the wrapper colors like ctx; "" below 8MB.
233
+ */
234
+ export function payloadSegment(bytes) {
235
+ if (!(bytes >= SHOW_FROM_BYTES)) return "";
236
+ const pct = Math.floor((bytes / REQUEST_LIMIT_BYTES) * 100);
237
+ return `payload:${pct}%/${(bytes / MB).toFixed(1)}MB`;
238
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pdf-vision-extract
3
- description: "(banker) 비주얼 위주 PDF를 고해상도 per-page PNG로 변환해 레이아웃 손실 없이 비전 입력으로 읽기. 'pdf-vision-extract'/'PDF를 이미지로'/'디자인 PDF 추출' 시 사용."
3
+ description: "그림 위주의 PDF 를 쪽마다 고해상도 PNG 로 바꿔 레이아웃 손실 없이 비전 입력으로 읽음. 'pdf-vision-extract'/'PDF를 이미지로'/'디자인 PDF 추출' 시 사용."
4
4
  ---
5
5
 
6
6
  # pdf-vision-extract
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: play-qa
3
- description: "(banker) Godot HTML5 웹게임을 실제 브라우저에서 직접 플레이하며 렌더·입력·시작 결함을 QA(playwright). 'play-qa'/'게임 직접 qa'/'네가 직접 플레이해봐'/'in-browser 플레이테스트' 시 사용."
3
+ description: "Godot 로 만든 HTML5 웹 게임을 실제 브라우저에서 Playwright 로 직접 플레이하며 렌더, 입력, 시작 결함을 찾음. 'play-qa'/'게임 직접 qa'/'네가 직접 플레이해봐'/'in-browser 플레이테스트' 시 사용."
4
4
  ---
5
5
 
6
6
  # play-qa: 에이전트가 직접 브라우저에서 게임을 플레이·QA