@kaydash9999/banker-plugins 0.20.2 → 0.21.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 (102) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +203 -1
  4. package/PRIVACY.md +34 -4
  5. package/README.md +46 -15
  6. package/bin/banker.js +228 -51
  7. package/bin/lib/codex-hooks.mjs +249 -0
  8. package/bin/lib/telemetry-config.mjs +33 -16
  9. package/codex/manifest.json +14 -11
  10. package/codex/skills/graceful-pause/SKILL.md +30 -0
  11. package/codex/skills/progress/SKILL.md +42 -0
  12. package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
  13. package/codex/skills/progress/scripts/progress.mjs +295 -0
  14. package/codex/transform-matrix.md +19 -14
  15. package/commands/setup.md +23 -0
  16. package/hooks/obsidize-hook.mjs +116 -4
  17. package/hooks/progress-plan.mjs +1 -1
  18. package/hooks/progress.mjs +143 -38
  19. package/hooks/register.mjs +13 -3
  20. package/hooks/telemetry-count-codex.mjs +222 -0
  21. package/hooks/update-checkin.mjs +15 -7
  22. package/hooks/update-fetch.mjs +15 -9
  23. package/hooks/update-notify.mjs +30 -17
  24. package/package.json +1 -1
  25. package/skills/3d-intro-build/SKILL.md +11 -5
  26. package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
  27. package/skills/3d-intro-build/references/curate.mjs +48 -14
  28. package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
  29. package/skills/3d-intro-build/references/prompts.md +4 -4
  30. package/skills/3d-intro-build/references/serve.mjs +6 -1
  31. package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
  32. package/skills/3d-intro-build/references/video-pool.mjs +250 -135
  33. package/skills/3d-intro-setup/SKILL.md +41 -10
  34. package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
  35. package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
  36. package/skills/anti-drm/SKILL.md +6 -3
  37. package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
  38. package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
  39. package/skills/append_wiki/SKILL.md +34 -8
  40. package/skills/audit-mock/SKILL.md +1 -1
  41. package/skills/audit-security/SKILL.md +64 -2
  42. package/skills/compact-wiki/SKILL.md +37 -6
  43. package/skills/deep-research/SKILL.md +11 -0
  44. package/skills/docs-setup/SKILL.md +37 -7
  45. package/skills/lfm/SKILL.md +148 -0
  46. package/skills/lineage/SKILL.md +25 -15
  47. package/skills/lineage/lineage.py +564 -301
  48. package/skills/motion-graphic-make/SKILL.md +1 -1
  49. package/skills/motion-graphic-make/references/curate.mjs +48 -14
  50. package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
  51. package/skills/motion-graphic-make/references/serve.mjs +6 -1
  52. package/skills/motion-graphic-setup/SKILL.md +58 -13
  53. package/skills/obsidizer/SKILL.md +30 -11
  54. package/skills/obsidizer/obsidize.mjs +50 -12
  55. package/skills/omc-patch/SKILL.md +86 -23
  56. package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
  57. package/skills/payload-mon/SKILL.md +6 -2
  58. package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
  59. package/skills/payload-mon/scripts/payload-size.mjs +4 -1
  60. package/skills/pdf-vision-extract/SKILL.md +11 -6
  61. package/skills/ralph-qa/SKILL.md +125 -151
  62. package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
  63. package/skills/ralph-qa/references/payload-scan.mjs +22 -7
  64. package/skills/ralph-qa/references/seat-payload.mjs +646 -0
  65. package/skills/ralph-qa/references/seat-run.mjs +956 -0
  66. package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
  67. package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
  68. package/skills/rfp-author/SKILL.md +5 -3
  69. package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
  70. package/skills/setup-bypass-permissions/SKILL.md +27 -9
  71. package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
  72. package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
  73. package/skills/setup-harness-factory/SKILL.md +17 -1
  74. package/skills/setup-insane-search/SKILL.md +16 -0
  75. package/skills/setup-java/SKILL.md +20 -1
  76. package/skills/setup-lsp/SKILL.md +56 -13
  77. package/skills/setup-mcp/SKILL.md +73 -4
  78. package/skills/setup-node/SKILL.md +31 -6
  79. package/skills/setup-omc/SKILL.md +24 -0
  80. package/skills/setup-omc-hud/SKILL.md +42 -9
  81. package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
  82. package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
  83. package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
  84. package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
  85. package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
  86. package/skills/setup-playwright/SKILL.md +45 -2
  87. package/skills/setup-pwsh/SKILL.md +19 -0
  88. package/skills/setup-python/SKILL.md +45 -7
  89. package/skills/setup-sandbox/SKILL.md +30 -5
  90. package/skills/setup-stitch/SKILL.md +22 -0
  91. package/skills/setup-tmux/SKILL.md +24 -4
  92. package/skills/smart-compact/SKILL.md +58 -12
  93. package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
  94. package/skills/tone-compact/SKILL.md +2 -1
  95. package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
  96. package/skills/ultra-chain/SKILL.md +12 -7
  97. package/skills/ultra-chain/references/preview-lib.mjs +51 -15
  98. package/skills/ultra-chain/references/serve.mjs +6 -1
  99. package/skills/ultra-interview/SKILL.md +14 -2
  100. package/skills/update-banker/SKILL.md +19 -3
  101. package/skills/vertical-pptx/SKILL.md +6 -3
  102. package/skills/vertical-pptx-setup/SKILL.md +76 -20
@@ -35,15 +35,17 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
35
35
  - **Codex**: Codex 자신의 요청 크기는 재지 않는다. 32MB 한도와 세션 파일 형식은 Claude Code 의 것이기 때문이다.
36
36
  Codex 에서 실행해도 같은 머신의 Claude Code HUD 래퍼를 켜고 끄는 것만 하고, `status` 의 "현재 세션" 줄은 Claude Code 세션에서만 나온다.
37
37
  사용자가 Codex 쪽 표시를 기대하면 이 점을 먼저 알린다.
38
+ Codex 의 `workspace-write` 샌드박스는 `~/.claude` 쓰기를 막는다. `status` 는 읽기만 해 그대로 된다. `on`, `off` 는 샌드박스 밖 실행으로 승인을 요청해 돌린다.
39
+ 승인 없이 돌리면 종료 1 과 `EROFS` 로 끝나고 아무것도 바뀌지 않는다(승인한 실행은 Codex 0.144.5 에서 확인). 승인을 요청할 수 없거나(승인 정책 `never`) 거부되면, 같은 명령을 Codex 입력창에서 `!` 를 붙여 사용자가 직접 실행하게 안내한다(예: `!node "<경로>" on`). 사용자가 친 `!` 명령은 샌드박스 밖에서 돈다(Codex 0.144.5, `-a never` 에서 실측). `codex exec` 처럼 사람이 없는 실행이면 그 명령을 보고하고 멈춘다. `--add-dir ~/.claude` 처럼 설정 폴더를 통째로 여는 방법은 쓰지 않는다. 그 세션이 훅과 설정을 고칠 수 있게 되어 샌드박스가 무력해진다.
38
40
 
39
41
  ## 동작
40
42
 
41
43
  - `on`: Claude Code 설정 폴더(`$CLAUDE_CONFIG_DIR`, 없으면 `~/.claude`)의 `hud/omc-hud-custom.mjs` 에서, ctx 를 넣는 줄 바로 뒤에 `// >>> payload-mon >>>` 블록 하나를 넣는다.
42
44
  블록이 불러오는 추정 모듈은 래퍼 옆 `hud/payload-mon/payload-size.mjs` 에 사본으로 둔다. 플러그인 설치 경로는 버전마다 바뀌어 업데이트 뒤에 사라지기 때문이다.
43
- 래퍼 파일이 다른 곳을 가리키는 링크면 사본은 그 파일 옆에 둔다. 래퍼 v4 가 링크를 푼 자리에서 사본을 찾기 때문이다.
45
+ 래퍼 파일이 다른 곳을 가리키는 링크면 사본은 그 파일 옆에 둔다. 래퍼 v4 가 그 자리에서 사본을 먼저 찾기 때문이다.
44
46
  이미 켜져 있으면 래퍼를 바꾸지 않고, 모듈 사본이 이 버전과 다를 때만 사본을 맞춘다. 예전 방식(블록 2개)으로 켜져 있었다면 새 방식으로 바꾼다.
45
47
  바꾸기 전 내용은 `omc-hud-custom.mjs.payload-mon.bak` 에 남는다.
46
- - `off`: 그 블록만 빼서 래퍼를 켜기 전 상태로 되돌리고, 모듈 사본과 추정치 캐시를 지운다. 래퍼 v4 에서는 모듈 사본이 없어져 `PL:` 칸도 사라진다.
48
+ - `off`: 그 블록만 빼서 래퍼를 켜기 전 상태로 되돌리고, 모듈 사본과 추정치 캐시를 지운다. 래퍼 v4 에서는 모듈 사본이 없어져 `PL:` 칸도 사라진다. 사본 폴더가 링크면 사본을 지우지 않고 그렇다고 알린다(`PL:` 칸이 남는다).
47
49
  - `status`: 켜짐·꺼짐, 모듈 사본이 이 버전과 같은지, statusLine 이 이 래퍼를 쓰는지, 현재 세션의 추정치를 보여 준다.
48
50
  - 같은 래퍼에 자기 표시 블록을 넣는 다른 도구가 있어도 서로의 블록을 건드리지 않는다.
49
51
 
@@ -61,6 +63,8 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
61
63
  - "node --check 실패": 블록을 넣은 결과가 문법 검사를 통과하지 못했다(예: 래퍼가 ctx 를 넣는 줄을 비동기가 아닌 함수 안으로 옮김). 파일은 그대로다.
62
64
  - "래퍼에 쓸 권한이 없습니다", 또는 `status` 의 "HUD 래퍼를 읽을 수 없음": 래퍼 파일의 권한을 확인한다. 파일은 그대로다.
63
65
  - "모듈 사본을 쓰지 못했습니다": 래퍼 옆(래퍼 파일이 링크면 링크가 가리키는 파일 옆) `payload-mon/` 에 쓸 수 없다(권한, 같은 이름의 파일, 디스크 공간). 래퍼는 그대로다.
66
+ `사본 폴더가 링크입니다` 면 그 폴더가 다른 곳을 가리키는 링크다. 이 도구는 링크 너머에 쓰거나 지우지 않는다. 링크를 일반 폴더로 바꾼 뒤 다시 켠다.
67
+ `사본 폴더가 쓰는 동안 바뀌었습니다` 면 쓰는 사이 그 폴더가 다른 것(링크, 다른 폴더)으로 바뀌었거나, 바뀌었다 돌아와 사본이 폴더에 없다. 바뀐 자리의 사본은 이 도구가 쓴 그 파일이고 바이트도 같을 때만 거뒀다(원래 거기 있던 다른 래퍼의 사본은 둔다). 바뀌었다 돌아온 사이 다른 곳에 간 사본은 거두지 못한다. 폴더를 확인한 뒤 다시 켠다.
64
68
  - "표시(>>> / <<<)의 짝이 맞지 않습니다": 래퍼에 payload-mon 표시 줄이 한쪽만 남았다. `on`·`off` 모두 거부하므로, 손으로 고치지 말라는 원칙의 유일한 예외다.
65
69
  `setup-omc-hud` 로 HUD 를 재설치하거나, 래퍼에서 짝 없이 남은 `// >>> payload-mon >>>` 또는 `// <<< payload-mon <<<` 줄을 지운 뒤 다시 실행한다.
66
70
  - HUD 를 재설치한 뒤 payload 가 안 보이면 `on` 을 다시 실행한다. `setup-omc-hud` 는 설치할 때마다 이 스킬을 늘 켠다(래퍼 v4 의 `PL:` 칸이 모듈 사본을 씀).
@@ -26,7 +26,9 @@
26
26
  * import pointing there would go dark without a word. `on` writes the copy and
27
27
  * refreshes it when this version's module differs; `status` says when it does.
28
28
  * When the wrapper file itself is a symlink, the copy goes beside the file it
29
- * points at, where wrapper v4 looks.
29
+ * points at, where wrapper v4 looks first. A copy folder (payload-mon/) that is
30
+ * itself a link is neither written nor emptied through: `on` refuses it, and
31
+ * also refuses (taking its copy back) when the folder changes while it writes.
30
32
  *
31
33
  * The block takes the ctx line's line ending, so a CRLF wrapper stays CRLF, and
32
34
  * a block whose line endings an editor rewrote still counts as on. A wrapper
@@ -63,7 +65,10 @@ import { CACHE_DIR, clearCache, estimatePayloadBytes, REQUEST_LIMIT_BYTES, SHOW_
63
65
 
64
66
  const OPEN = "// >>> payload-mon >>>";
65
67
  const CLOSE = "// <<< payload-mon <<<";
66
- const BLOCKS = /^[ \t]*\/\/ >>> payload-mon >>>[\s\S]*?\/\/ <<< payload-mon <<<[ \t]*\r?\n/gm;
68
+ export const BLOCKS = /^[ \t]*\/\/ >>> payload-mon >>>[\s\S]*?\/\/ <<< payload-mon <<<[ \t]*\r?\n/gm;
69
+ // The closing line. No block reaches past the last one, so the pattern runs only on the text before it:
70
+ // otherwise every stray opening line scans to the end again (a 2MB file of them took 28s).
71
+ export const CLOSE_LINE = /\/\/ <<< payload-mon <<<[ \t]*\r?\n/g;
67
72
  // The block goes right after the line that adds ctx.
68
73
  const CTX_LINE = /^ {2}if \(f\.ctx\) colored\.push\(colorCtx\(f\.ctx\)\);\r?\n/m;
69
74
  // The statusline JSON the block reads, in either layout.
@@ -139,6 +144,13 @@ const insertBlock = (base, url) => base.replace(CTX_LINE, (line) => line + place
139
144
 
140
145
  const count = (text, needle) => text.split(needle).length - 1;
141
146
 
147
+ // The text without its blocks, the pattern run only up to the last closing line (see CLOSE_LINE).
148
+ function withoutBlocks(src) {
149
+ let end = -1;
150
+ for (const m of src.matchAll(CLOSE_LINE)) end = m.index + m[0].length;
151
+ return end < 0 ? src : src.slice(0, end).replace(BLOCKS, "") + src.slice(end);
152
+ }
153
+
142
154
  // The wrapper's text, null when there is no such file. Any other read failure
143
155
  // (permissions, say) is not "missing" and propagates.
144
156
  const readText = (file) => {
@@ -174,7 +186,7 @@ function blockEol(src, url) {
174
186
  // not pair up would let the block pattern swallow the code between them.
175
187
  function misfit(src) {
176
188
  if (count(src, OPEN) !== count(src, CLOSE)) return "payload-mon 표시(>>> / <<<)의 짝이 맞지 않습니다";
177
- const base = src.replace(BLOCKS, "");
189
+ const base = withoutBlocks(src);
178
190
  if (!base.includes("omc-hud.mjs") || !base.includes("const colorRate")) return "OMC 커스텀 HUD 래퍼가 아닙니다";
179
191
  if (!INPUT_DECL.test(base)) return "삽입 위치를 찾지 못했습니다(statusline 입력을 담는 input 변수가 없음)";
180
192
  if (!CTX_LINE.test(base)) return "삽입 위치를 찾지 못했습니다(ctx를 넣는 줄이 없음)";
@@ -189,6 +201,16 @@ function copyState(o) {
189
201
  return source !== null && copy.equals(source) ? "current" : "stale";
190
202
  }
191
203
 
204
+ // Whether the folder holding `file` is a link. The copy folder is this tool's, and a link there
205
+ // would take the copy, or its removal, to wherever it points.
206
+ function linkedFolder(file) {
207
+ try {
208
+ return lstatSync(dirname(file)).isSymbolicLink();
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
192
214
  // Replaces file through a fresh temp file ("wx") and a rename: a link planted at
193
215
  // either name is replaced rather than written through, and a write that fails
194
216
  // leaves the old file as it was.
@@ -197,24 +219,62 @@ function writeFresh(file, data, mode) {
197
219
  rmSync(tmp, { force: true });
198
220
  try {
199
221
  writeFileSync(tmp, data, { flag: "wx", mode });
222
+ const id = fileId(tmp);
200
223
  renameSync(tmp, file);
224
+ return id;
201
225
  } catch (e) {
202
226
  rmSync(tmp, { force: true });
203
227
  throw e;
204
228
  }
205
229
  }
206
230
 
231
+ // A file's identity, exact: NTFS file IDs pass 2^53, where a Number loses the low bits.
232
+ const fileId = (p) => {
233
+ const s = lstatSync(p, { bigint: true });
234
+ return `${s.dev}:${s.ino}`;
235
+ };
236
+ // The copy folder's identity, to tell afterwards whether it is still the folder that was written into.
237
+ const folderStamp = (dir) => {
238
+ const s = lstatSync(dir, { bigint: true });
239
+ return `${s.dev}:${s.ino}:${s.isSymbolicLink()}`;
240
+ };
241
+ // After the write: the copy folder must still be the one stamped before, holding these bytes. Another program
242
+ // may have swapped it (for a link, say), so the copy went wherever it led, or put it back after the write, so
243
+ // the copy is not in it. Then the write is refused, and the copy is taken back from where the folder now leads
244
+ // when the file there is the one written (`written`, from writeFresh) and still ours byte for byte: a copy that
245
+ // was there before, or one another program wrote over, stays.
246
+ // Node has no openat, so a copy that went elsewhere through a swap that was put back stays where it went, and
247
+ // a swap after this check goes unseen.
248
+ function checkFolder(copy, stamp, data, written) {
249
+ const dir = dirname(copy);
250
+ if (folderStamp(dir) === stamp && readBytes(copy)?.equals(data)) return;
251
+ try {
252
+ const landed = join(realpathSync(dir), basename(copy));
253
+ if (fileId(landed) === written && readFileSync(landed).equals(data)) rmSync(landed, { force: true });
254
+ } catch {
255
+ /* nothing there to take back */
256
+ }
257
+ throw new Error(`사본 폴더가 쓰는 동안 바뀌었습니다: ${dir}`);
258
+ }
259
+
207
260
  // Brings the copy up to this version's module; true when it had to write. The
208
261
  // statusline imports it on every refresh, so no other account may change it
209
262
  // (reading it is harmless). When the write fails, a folder made here is taken
210
263
  // away again: mkdirSync names a folder only when it created it, and rmdir
211
264
  // leaves one that something else has meanwhile put a file in.
212
265
  function syncModule(o) {
213
- if (copyState(o) === "current") return false;
214
266
  const copy = moduleCopy(o.hudFile);
267
+ if (linkedFolder(copy)) throw new Error(`사본 폴더가 링크입니다: ${dirname(copy)}`);
268
+ if (copyState(o) === "current") return false;
269
+ // Read first: a module that cannot be read leaves no folder behind.
270
+ const data = readFileSync(o.moduleSource);
215
271
  const made = mkdirSync(dirname(copy), { recursive: true, mode: 0o755 }) !== undefined;
216
272
  try {
217
- writeFresh(copy, readFileSync(o.moduleSource), 0o644);
273
+ const stamp = folderStamp(dirname(copy));
274
+ // From the same lstat: a folder that became a link since the check above is refused before the write.
275
+ if (stamp.endsWith(":true")) throw new Error(`사본 폴더가 링크입니다: ${dirname(copy)}`);
276
+ const written = writeFresh(copy, data, 0o644);
277
+ checkFolder(copy, stamp, data, written);
218
278
  } catch (e) {
219
279
  if (made) {
220
280
  try {
@@ -307,23 +367,26 @@ export function turnOn(opts = {}) {
307
367
  const done = showsPL(src)
308
368
  ? "켰습니다. 다음 statusline 갱신부터 PL 칸에 요청 크기가 늘 표시됩니다."
309
369
  : "켰습니다. 다음 statusline 갱신부터 8MB 이상이면 ctx 옆에 payload가 표시됩니다.";
310
- const after = insertBlock(src.replace(BLOCKS, ""), moduleUrl(o.hudFile));
370
+ const after = insertBlock(withoutBlocks(src), moduleUrl(o.hudFile));
311
371
  return commit(o.hudFile, src, after, done + notShownNote(o), () => syncModule(o));
312
372
  }
313
373
 
314
374
  function removeBlocks(o, src) {
315
375
  if (src === null) return { ok: true, message: `HUD 래퍼가 없어 끌 것이 없습니다: ${o.hudFile}` };
316
376
  if (count(src, OPEN) !== count(src, CLOSE)) return refusal("payload-mon 표시(>>> / <<<)의 짝이 맞지 않습니다", o.hudFile);
317
- const out = src.replace(BLOCKS, "");
377
+ const out = withoutBlocks(src);
318
378
  if (out === src) return { ok: true, message: "이미 꺼져 있습니다 (래퍼 변경 없음)." };
319
379
  return commit(o.hudFile, src, out, "껐습니다. 다음 statusline 갱신부터 payload가 표시되지 않습니다.");
320
380
  }
321
381
 
322
382
  // What off leaves behind otherwise: the cache (when this account controls its
323
383
  // folder), and the module copy with its folder (only when nothing else is in
324
- // it), also the one 0.20.1 left beside a linked wrapper. None failing undoes off.
384
+ // it), also the one 0.20.1 left beside a linked wrapper, but nothing through a copy folder that is a link.
385
+ // None failing undoes off.
386
+ // Returns the copy folders it left alone because they are links.
325
387
  function removeLeftovers(o) {
326
- const copies = [...new Set([moduleCopy(o.hudFile), besideLink(o.hudFile)])];
388
+ const all = [...new Set([moduleCopy(o.hudFile), besideLink(o.hudFile)])];
389
+ const copies = all.filter((copy) => !linkedFolder(copy));
327
390
  for (const remove of [() => clearCache(o.cacheDir), ...copies.map((copy) => () => rmSync(copy, { force: true }))]) {
328
391
  try {
329
392
  remove();
@@ -338,14 +401,18 @@ function removeLeftovers(o) {
338
401
  /* absent, or holds something that is not ours */
339
402
  }
340
403
  }
404
+ return all.filter((copy) => !copies.includes(copy)).map((copy) => dirname(copy));
341
405
  }
342
406
 
343
407
  export function turnOff(opts = {}) {
344
408
  const o = { ...DEFAULTS, ...opts };
345
409
  const res = removeBlocks(o, readText(o.hudFile));
346
- if (res.ok) removeLeftovers(o);
347
- return res;
410
+ if (!res.ok) return res;
411
+ // A copy kept in a linked folder still feeds wrapper v4's PL segment, so off says so.
412
+ const kept = removeLeftovers(o);
413
+ return kept.length ? { ...res, message: `${res.message} ${KEPT_NOTE}: ${kept.join(", ")}` } : res;
348
414
  }
415
+ const KEPT_NOTE = "사본 폴더가 링크라 모듈 사본은 지우지 않았습니다(래퍼 v4 의 PL 칸이 계속 보일 수 있음)";
349
416
 
350
417
  // The estimate for the session that runs this, "" if unknown, ending with where
351
418
  // it shows when `where` names it:
@@ -378,7 +445,16 @@ function stateOf(src, o) {
378
445
  // on and off both refuse this, so pointing at on would be a dead end.
379
446
  if (count(src, OPEN) !== count(src, CLOSE)) return "표시(>>> / <<<)의 짝이 맞지 않습니다 (on·off 모두 거부: HUD 재설치 또는 남은 표시 줄 정리)";
380
447
  if (blockEol(src, moduleUrl(o.hudFile)) === "") return "켜짐이 불완전하거나 오래됨 (on 으로 복구)";
381
- return "켜짐" + COPY_NOTE[copyState(o)];
448
+ return "켜짐" + copyNote(o);
449
+ }
450
+
451
+ // What status adds about the module copy. In a copy folder that is a link, on refuses to write it, so
452
+ // pointing at on would be a dead end.
453
+ function copyNote(o) {
454
+ const state = copyState(o);
455
+ const copy = moduleCopy(o.hudFile);
456
+ if (state !== "current" && linkedFolder(copy)) return `, 단 사본 폴더가 링크라 모듈 사본을 쓰지 않음 (링크를 일반 폴더로 바꾼 뒤 on): ${dirname(copy)}`;
457
+ return COPY_NOTE[state];
382
458
  }
383
459
 
384
460
  // What status says about the wrapper. A wrapper it cannot read is reported, not
@@ -211,7 +211,10 @@ export function clearCache(dir = CACHE_DIR) {
211
211
  export function estimatePayloadBytes(transcriptPath, cacheDir = CACHE_DIR) {
212
212
  const fd = openSync(transcriptPath, "r");
213
213
  try {
214
- const { size, ino } = fstatSync(fd);
214
+ // The file number as a string: NTFS file IDs pass 2^53, where a Number loses the low bits, and JSON has no BigInt.
215
+ const st = fstatSync(fd, { bigint: true });
216
+ const size = Number(st.size);
217
+ const ino = String(st.ino);
215
218
  const file = cacheFile(cacheDir, transcriptPath);
216
219
  const c = loadCache(file);
217
220
  // Resume only a cache of these rules, for this file, which has not shrunk since.
@@ -35,7 +35,7 @@ Target: **long edge = 2400 px** (just under cap, leaves headroom).
35
35
 
36
36
  ```bash
37
37
  ls -lh <pdf-path>
38
- python3 -c "import fitz; d=fitz.open('<pdf-path>'); print('pages=', d.page_count)"
38
+ (cd ~ && python3 -c "import fitz; d=fitz.open('<absolute-pdf-path>'); print('pages=', d.page_count)") # from home: -c, -m and a stdin script import from the current folder first
39
39
  ```
40
40
 
41
41
  ### 2. Choose output directory
@@ -49,7 +49,7 @@ Convention: place PNGs **next to** existing design assets, in a subfolder named
49
49
  PyMuPDF is the most reliable single-dependency path (no Poppler, no ImageMagick).
50
50
 
51
51
  ```bash
52
- python3 - <<'PY'
52
+ (cd ~ && python3 - <<'PY'
53
53
  import fitz, os
54
54
  SRC = "<absolute-pdf-path>"
55
55
  OUT = "<absolute-output-dir>"
@@ -63,9 +63,12 @@ for i, page in enumerate(doc, start=1):
63
63
  pix.save(f"{OUT}/page-{i:03d}.png")
64
64
  print(f"[{i}/{doc.page_count}] page-{i:03d}.png {pix.width}x{pix.height}")
65
65
  PY
66
+ )
66
67
  ```
67
68
 
68
- Install if missing: `python3 -m pip install --user pymupdf`
69
+ Install if missing: `(cd ~ && python3 -m pip install --user pymupdf)`
70
+
71
+ Offline (no network): do not install; look for the fallback tools of section 4 (`pdftoppm`, ImageMagick) instead and say "오프라인이라 설치하지 않음(필요한 것: pymupdf)". pip retries 5 times with a 15 s timeout by default before it fails.
69
72
 
70
73
  ### 4. Fallback toolchain
71
74
 
@@ -83,7 +86,7 @@ If Python/PyMuPDF unavailable:
83
86
 
84
87
  ```bash
85
88
  ls <OUT> | wc -l # page count matches PDF
86
- python3 -c "from PIL import Image; im=Image.open('<OUT>/page-001.png'); print(im.size)"
89
+ (cd ~ && python3 -c "from PIL import Image; im=Image.open('<absolute-output-dir>/page-001.png'); print(im.size)")
87
90
  ```
88
91
 
89
92
  Expect long edge ≈ 2400 px.
@@ -110,10 +113,12 @@ For multiple pages in one turn, batch multiple `Read` calls in parallel.
110
113
  For a hybrid pipeline (visual + searchable text), also extract markdown:
111
114
 
112
115
  ```bash
113
- python3 -m pip install --user pymupdf4llm
114
- python3 -c "import pymupdf4llm; print(pymupdf4llm.to_markdown('<pdf>'))" > <OUT>/text.md
116
+ (cd ~ && python3 -m pip install --user pymupdf4llm)
117
+ (cd ~ && python3 -c "import pymupdf4llm; print(pymupdf4llm.to_markdown('<absolute-pdf-path>'))") > <OUT>/text.md
115
118
  ```
116
119
 
120
+ The install needs the network too; offline, leave the markdown companion out and say so.
121
+
117
122
  Then `grep` the `.md` for labels and cross-reference with the PNGs.
118
123
 
119
124
  ## Example invocation history