@kaydash9999/banker-plugins 0.19.5 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code skills + commands — QA, audit, docs, architecture, dependency setup, and wiki; the banker plugin marketplace.",
9
- "version": "0.19.5"
9
+ "version": "0.20.0"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "banker",
3
- "version": "0.19.5",
3
+ "version": "0.20.0",
4
4
  "description": "Claude Code plugin: QA/audit/docs/architecture skills + commands, a /banker:setup installer for OS dependencies (playwright, etc.), and wiki compaction.",
5
5
  "author": {
6
6
  "name": "smallOpenSource",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.20.0] - 2026-10-19
4
+
5
+ ### Changed
6
+ - `setup-omc-hud` 가 상태표시줄 파일을 내려받지 않고 스킬에 든 파일(`skills/setup-omc-hud/assets/`)로 설치한다. 모든 OS 에서 `node assets/apply-hud.mjs` 한 명령이고, 네트워크가 없어도 끝까지 된다. 예전에는 GitHub 의 omc_hud 저장소에서 OS 별 스크립트를 받아, 원본이 바뀌면 설치 결과도 말없이 바뀌었다.
7
+ - 설치 스크립트는 설정 폴더 경로의 공백과 `settings.json` 앞의 BOM 을 처리하고, 손으로 고친 래퍼와 HUD 설정은 `.backup.<시각>` 으로 남긴다. 백업은 원본의 바이트와 권한을 그대로 두고, statusLine 의 다른 필드(`padding` 등)는 남긴다.
8
+ - 설치 스크립트는 `settings.json` 이 JSON 객체가 아니거나, OMC 가 없거나, 설정 폴더 경로에 셸이 해석하는 글자(`"`, `$`, 백틱, `%`, `\`, 줄바꿈)가 있거나, 쓸 수 없는 파일이 있으면 아무 파일도 바꾸지 않고 멈춘다(종료 1). 예기치 못한 오류로 중간에 멈추면 종료 2 로 알린다.
9
+ - 첨부 HUD 설정은 OMC 출력을 8줄까지 둔다. 3줄이면 에이전트가 하나만 돌아도 갱신 안내와 함께 상세 줄이 `... (+N lines)` 로 접혔다.
10
+ - 상태표시줄 래퍼가 v4 로 바뀐다. 첫 줄은 `<model>/<effort>|5h:<pct>/<reset>|wk:<pct>/<reset>|<mm>:<pct>/<reset>|think|PL:<mb>M|se:<hours>h|ctx:<pct>%|🤖<agents>|🔧<tools>|⚡<skills>|<cwd>|<account>|<branch>|claude<cur>→<new>` 이고, 둘째 줄부터 에이전트 목록 같은 상세 줄이 붙는다. 화면 폭을 넘으면 칸 사이에서 접는다. OMC 와 Claude Code 의 갱신 안내 줄, OMC 판 표시는 보이지 않는다.
11
+ - `setup-omc-hud` 는 설치할 때마다 payload-mon 을 켠다. `PL:` 칸이 payload-mon 의 크기 계산 모듈을 쓰기 때문이다. 예전에는 켜져 있던 경우만 다시 켰다.
12
+ - `setup-omc-hud` 의 Claude 갱신 안내 블록 단계를 뺀다. 새 래퍼가 안내를 줄여 직접 맨 끝에 둔다. 인자 `off`, `status` 는 옛 래퍼에 넣은 블록을 위해 남는다.
13
+ - 유지보수자용: 설치 스크립트, 래퍼 형식, 블록 도구 둘과 래퍼 v4 의 왕복을 시험한다. smoke 는 스킬 설명서에 내려받기 명령이 없는지와 Codex 사본의 설치 스크립트가 도는지를 본다.
14
+
15
+ ### Fixed
16
+ - `payload-mon` 과 `setup-omc-hud` 의 블록 도구가 따옴표 안에서 펼쳐지지 않은 `~` 나 끝 공백이 든 `CLAUDE_CONFIG_DIR` 를 그대로 경로로 써서, 설치 스크립트가 만든 래퍼를 찾지 못하던 것. 이제 설치 스크립트와 같이 `~/x` 를 `<홈>/x` 로 읽는다.
17
+
3
18
  ## [0.19.5] - 2026-10-17
4
19
 
5
20
  ### Changed
package/README.md CHANGED
@@ -180,7 +180,7 @@ USER_RESOURCES 가이드의 공통 요소를 OS별·런타임별(Claude Code/Cod
180
180
  | `setup-omc` | oh-my-claudecode(OMC) 설치·갱신 (Codex는 OMX) |
181
181
  | `setup-harness-factory` | revfactory/harness 팀 아키텍처 팩토리 설치·구성·사용안내 (Codex=meta-harness) |
182
182
  | `setup-playwright` | Playwright + headless 브라우저 (RHEL8/Rocky8·non-root·no-conda 폴백) |
183
- | `setup-omc-hud` | omc_hud 상태표시줄 (OS별). Claude 갱신 안내는 줄 맨 끝 |
183
+ | `setup-omc-hud` | omc_hud 상태표시줄(래퍼 v4). 스킬에 든 파일로 오프라인 설치, 모든 OS 같은 명령 |
184
184
  | `setup-insane-search` | insane-search 플러그인 설치 (Claude·Codex) |
185
185
  | `setup-stitch` | Stitch 디자인 MCP 프록시 등록(RockyLinux8 proxy) |
186
186
  | `docs-setup` | arch-diagram·pdf-vision-extract 의존성(pptx·pymupdf·plantuml) 설치 |
@@ -247,7 +247,7 @@ banker uninstall # 제거
247
247
  | `setup-mcp` | `claude mcp add` (context7·seq-thinking·filesystem·git·fetch) | `[mcp_servers.*]` |
248
248
  | `setup-lsp` | LSP MCP 등록 | `[mcp_servers.lsp_bridge]` |
249
249
  | `setup-stitch` | `claude mcp add stitch` | `codex mcp add stitch` |
250
- | `setup-omc-hud` | `statusLine` + `hud/omc-hud-custom.mjs` 에 Claude 갱신 안내를 줄 맨 끝으로 옮기는 블록 1개(`.claude-update-last.bak` 백업·멱등·`node --check` 검증, `off` 로 원복) | — (Codex는 OMX `hud`) |
250
+ | `setup-omc-hud` | `statusLine`(`settings.json` 백업) + `hud/omc-hud-custom.mjs`, `hud/omc-hud.mjs`, `.omc/hud-config.json`(바뀐 파일은 `.backup.<시각>` 백업) + payload-mon 켜기. 옛 판이 넣은 갱신 안내 블록은 `off` 로 원복 | — (Codex는 OMX `hud`) |
251
251
  | `setup-pwsh` | `env.CLAUDE_CODE_GIT_BASH_PATH` (병합) | — (네이티브 셸, 배선 불필요) |
252
252
  | `setup-harness-factory` | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` (env 영속) | — |
253
253
  | `smart-compact` | `statusLine.command` 래핑 + `UserPromptSubmit` 훅 추가 (백업·멱등) | `~/.codex/` 대응 |
@@ -301,7 +301,8 @@ Codex는 재설치할 때마다 기존 `banker-*` 를 먼저 정리하므로 옛
301
301
  > 표시 블록과 모듈 사본은 플러그인 밖(`~/.claude/hud/`)에 있어서, 플러그인을 지워도 남아 계속 표시됩니다.\
302
302
  > 이미 지웠다면 HUD를 재설치하거나, `omc-hud-custom.mjs` 에서 `// >>> payload-mon >>>` 부터 `// <<< payload-mon <<<` 까지 지우면 됩니다.
303
303
 
304
- > `setup-omc-hud` 가 넣은 Claude 갱신 안내 블록도 플러그인 밖(`~/.claude/hud/omc-hud-custom.mjs`)에 남습니다. 원래 순서로 되돌리려면 제거 전에 `/banker:setup-omc-hud off` 를 실행하세요.\
304
+ > `setup-omc-hud` 가 설치한 상태표시줄 래퍼와 설정도 플러그인 밖(`~/.claude/hud/`, `~/.claude/.omc/`)에 있어서, 플러그인을 지워도 남습니다.\
305
+ > 0.19.5 이하의 `setup-omc-hud` 가 옛 래퍼에 넣은 Claude 갱신 안내 블록은 제거 전에 `/banker:setup-omc-hud off` 로 뺄 수 있습니다.\
305
306
  > 이미 지웠다면 HUD를 재설치하거나, `// >>> claude-update-last >>>` 부터 `// <<< claude-update-last <<<` 까지 지우면 됩니다. 블록을 남겨 두어도 상태표시줄은 깨지지 않습니다.
306
307
 
307
308
  > `setup-bypass-permissions` 로 바꾼 권한 모드는 `settings.json` 에 남아, 플러그인을 지워도 모든 세션에서 확인 없이 도구가 실행됩니다. 제거 전에 `/banker:setup-bypass-permissions off` 를 실행하세요.\
@@ -366,6 +367,8 @@ banker 자체는 **MIT** ([LICENSE](LICENSE)). Owner: [smallOpenSource](https://
366
367
  패턴 목록은 Wikipedia [*Signs of AI writing*](https://en.wikipedia.org/wiki/Wikipedia:Signs_of_AI_writing)(WikiProject AI Cleanup, **CC BY-SA**)에 기반합니다.
367
368
  - `skills/3d-intro-build/references`: [oso95/scroll-world](https://github.com/oso95/scroll-world) 의 스크럽 엔진(`scrub-engine.js`·`index-template.html`)을 그대로 벤더링, **MIT License**(© 2026 cyw).\
368
369
  원본 라이선스 고지는 `skills/3d-intro-build/references/LICENSE` 와 `NOTICE.md` 에 포함.
370
+ - `skills/setup-omc-hud/assets`: 상태표시줄 래퍼와 설치 스크립트는 [smallOpenSource/omc_hud](https://github.com/smallOpenSource/omc_hud) d3cedd5 를 고친 것, **MIT License**(© 2026 smallOpenSource).\
371
+ 원본 라이선스 고지는 `skills/setup-omc-hud/assets/LICENSE-omc_hud` 에 포함.
369
372
  - `skills/theme-selection/assets`: 번들 팔레트(`palettes.json`)는 [VoltAgent/awesome-design-md](https://github.com/VoltAgent/awesome-design-md) 의 DESIGN.md 에서 색 값을 읽어 만든 것, **MIT License**(© 2026 VoltAgent).\
370
373
  원본 라이선스 고지는 `skills/theme-selection/assets/LICENSE` 와 `NOTICE.md` 에 포함.\
371
374
  테마 이름의 상표는 각 소유자에게 있으며, banker 는 그 회사들과 관계가 없습니다.
@@ -28,7 +28,7 @@
28
28
  { "name": "ultra-init", "type": "skill", "target": "both", "reason": "orchestrates ralplan->ralph->QA gate (no ultragoal ledger: OMC 5.6.1 checkpoints need a /goal snapshot that would compete with ralph); Claude=OMC 5 (verify cycle in place of the removed ultraqa; needs the omc CLI with `omc ralph verify`), Codex=OMX (OMX ultraqa); body runtime-aware", "supportingFiles": [] },
29
29
  { "name": "omc-reference", "type": "skill", "target": "both", "reason": "OMC/OMX agent/tool/skill registry reference; runtime-aware (Codex counterpart is OMX)", "supportingFiles": [] },
30
30
  { "name": "setup-omc", "type": "skill", "target": "both", "reason": "installs OMC (Claude) or OMX (Codex `omx setup`); body already dual-runtime", "supportingFiles": [] },
31
- { "name": "setup-omc-hud", "type": "skill", "target": "both", "reason": "installs omc_hud (Claude statusLine) and moves OMC's Claude Code update notice to the end of the line; Codex uses OMX `hud`; runtime-aware", "supportingFiles": ["scripts/claude-update-last.mjs"] },
31
+ { "name": "setup-omc-hud", "type": "skill", "target": "both", "reason": "installs the omc_hud statusline wrapper v4 (Claude statusLine) from files bundled in assets/, offline and with one node command on every OS, then turns payload-mon on for the PL segment; off/status handle the update-notice block older wrappers took; Codex uses OMX `hud`; runtime-aware", "supportingFiles": ["assets/apply-hud.mjs", "assets/omc-hud-custom.mjs", "assets/hud-config.json", "assets/LICENSE-omc_hud", "scripts/claude-update-last.mjs"] },
32
32
  { "name": "setup-stitch", "type": "skill", "target": "both", "reason": "registers the Stitch MCP proxy (RockyLinux8 ~/bin/stitch-proxy.sh); Claude `claude mcp add`, Codex `codex mcp add`", "supportingFiles": [] },
33
33
 
34
34
  { "name": "docs-setup", "type": "skill", "target": "both", "reason": "installs arch-diagram/pdf-vision-extract deps (python-pptx, pymupdf, plantuml); python-env detect/select; runtime-agnostic installer", "supportingFiles": [] },
package/commands/setup.md CHANGED
@@ -31,7 +31,7 @@ banker 플러그인이 제공하는 설치 스킬들을 **multi-select**로 골
31
31
  - **setup-omc** — oh-my-claudecode(OMC) 설치/갱신(Codex는 OMX). `all-in-one`·`ultra-chain`·`ultra-init`·`/banker:front-qa` 의존성.
32
32
  - **setup-harness-factory** — revfactory/harness(팀 아키텍처 팩토리) 설치+구성+안내(Codex=meta-harness).
33
33
  - **setup-insane-search** — insane-search 플러그인(차단 사이트 우회, Claude/Codex 양쪽).
34
- - **setup-omc-hud** — omc_hud 상태표시줄(OS별).
34
+ - **setup-omc-hud** — omc_hud 상태표시줄(래퍼 v4, 스킬에 든 파일로 오프라인 설치).
35
35
  - **setup-stitch** — Stitch(디자인 생성) MCP 프록시 등록(RockyLinux8 proxy-script, API key 필요).
36
36
 
37
37
  **의존성 라이브러리**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.19.5",
3
+ "version": "0.20.0",
4
4
  "description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
5
5
  "bin": {
6
6
  "banker": "bin/banker.js"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: payload-mon
3
- description: "OMC HUD 상태표시줄의 `ctx` 옆에, 현재 세션이 보내는 API 요청의 페이로드 추정치를 32MB 한도 대비로 표시하는 기능을 켜고 끔. 8MB 부터 표시. 'payload-mon'/'payload 모니터 켜/꺼'/'상태표시줄 payload 표시'/'요청 크기 경고'/'32MB 한도 경고'/'payload 표시가 안 보여' 시 사용."
3
+ description: "OMC HUD 상태표시줄에 현재 세션이 보내는 API 요청의 페이로드 추정치를 32MB 한도 대비로 표시하는 기능을 켜고 끔. 래퍼 v4 는 `PL:` 칸으로 늘 보이고, 옛 래퍼는 8MB 부터 `ctx` 옆에 보임. 'payload-mon'/'payload 모니터 켜/꺼'/'상태표시줄 payload 표시'/'요청 크기 경고'/'32MB 한도 경고'/'payload 표시가 안 보여' 시 사용."
4
4
  argument-hint: "on | off | status"
5
5
  ---
6
6
 
@@ -11,6 +11,7 @@ argument-hint: "on | off | status"
11
11
  Claude Code 는 요청 본문이 32MB 를 넘으면 "Request too large (max 32MB)" 로 거절한다.
12
12
  이 한도는 토큰이 아니라 바이트라서, ctx(컨텍스트 %)가 낮아도 이미지나 큰 도구 결과가 쌓이면 걸린다.
13
13
  이 스킬은 그 바이트를 추정해 한도의 25%(8MB)부터 ctx 옆에 `payload:28%/9.0MB` 처럼 보여 준다.
14
+ `setup-omc-hud` 가 까는 래퍼 v4 는 이 스킬의 모듈로 크기를 늘 `se` 앞에 `PL:0.5M` 처럼 보이고, 블록이 내는 칸은 그 `PL:` 이 대신한다.
14
15
 
15
16
  ## 실행
16
17
 
@@ -41,13 +42,14 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
41
42
  블록이 불러오는 추정 모듈은 래퍼 옆 `hud/payload-mon/payload-size.mjs` 에 사본으로 둔다. 플러그인 설치 경로는 버전마다 바뀌어 업데이트 뒤에 사라지기 때문이다.
42
43
  이미 켜져 있으면 래퍼를 바꾸지 않고, 모듈 사본이 이 버전과 다를 때만 사본을 맞춘다. 예전 방식(블록 2개)으로 켜져 있었다면 새 방식으로 바꾼다.
43
44
  바꾸기 전 내용은 `omc-hud-custom.mjs.payload-mon.bak` 에 남는다.
44
- - `off`: 그 블록만 빼서 래퍼를 켜기 전 상태로 되돌리고, 모듈 사본과 추정치 캐시를 지운다.
45
+ - `off`: 그 블록만 빼서 래퍼를 켜기 전 상태로 되돌리고, 모듈 사본과 추정치 캐시를 지운다. 래퍼 v4 에서는 모듈 사본이 없어져 `PL:` 칸도 사라진다.
45
46
  - `status`: 켜짐·꺼짐, 모듈 사본이 이 버전과 같은지, statusLine 이 이 래퍼를 쓰는지, 현재 세션의 추정치를 보여 준다.
46
47
  - 같은 래퍼에 자기 표시 블록을 넣는 다른 도구가 있어도 서로의 블록을 건드리지 않는다.
47
48
 
48
49
  ## 무엇을 어떻게 보여 주는가
49
50
 
50
- - `ctx:21%|payload:28%/9.0MB|se:…`: 32MB 대비 %(ctx 와 같은 색: 70% 미만 초록, 85% 미만 노랑, 그 이상 빨강)와 크기(MB, 청록). 8MB 미만이면 숨긴다.
51
+ - 래퍼 v4: `PL:9.0M|se:0.7h|…`. 크기(MB, 소수 1자리)를 늘 보이고, 색은 32MB 대비 %(70% 미만 초록, 85% 미만 노랑, 그 이상 분홍)다.
52
+ - 옛 래퍼: `ctx:21%|payload:28%/9.0MB|se:…`: 32MB 대비 %(ctx 와 같은 색: 70% 미만 초록, 85% 미만 노랑, 그 이상 빨강)와 크기(MB, 청록). 8MB 미만이면 숨긴다.
51
53
  - 값은 마지막 `/compact` 이후 Claude Code 가 실제로 보내는 메시지의 바이트다. Claude Code 가 32MB 한도를 판정하는 방식과 같고, 무엇을 넣고 뺄지는 Claude Code 2.1.288 의 요청 필터를 따른다.
52
54
  - 세션 파일 크기가 아니다. 71MB 짜리 세션 파일도 실제 전송분은 0.1MB 일 수 있다. 알려진 오차는 `scripts/payload-size.mjs` 머리 주석에 있다.
53
55
 
@@ -60,7 +62,7 @@ node "<이 스킬 디렉터리 절대경로>/scripts/payload-mon.mjs" <on|off|st
60
62
  - "모듈 사본을 쓰지 못했습니다": 래퍼 옆 `hud/payload-mon/` 에 쓸 수 없다(권한, 같은 이름의 파일, 디스크 공간). 래퍼는 그대로다.
61
63
  - "표시(>>> / <<<)의 짝이 맞지 않습니다": 래퍼에 payload-mon 표시 줄이 한쪽만 남았다. `on`·`off` 모두 거부하므로, 손으로 고치지 말라는 원칙의 유일한 예외다.
62
64
  `setup-omc-hud` 로 HUD 를 재설치하거나, 래퍼에서 짝 없이 남은 `// >>> payload-mon >>>` 또는 `// <<< payload-mon <<<` 줄을 지운 뒤 다시 실행한다.
63
- - HUD 를 재설치한 뒤 payload 가 안 보이면 `on` 을 다시 실행한다. `setup-omc-hud` 로 재설치했다면 그 스킬이 다시 켠다.
65
+ - HUD 를 재설치한 뒤 payload 가 안 보이면 `on` 을 다시 실행한다. `setup-omc-hud` 는 설치할 때마다 이 스킬을 늘 켠다(래퍼 v4 의 `PL:` 칸이 모듈 사본을 씀).
64
66
  - banker 업데이트 알림에 payload-mon 이 바뀌었다고 나오거나 `status` 에 "모듈 사본이 이 버전과 다름" 이 나오면, `on` 을 한 번 실행해 사본을 맞춘다.
65
67
  - `status` 에 "주의: statusLine 명령에 … 보이지 않습니다" 가 나오면 statusline 이 다른 명령이다. smart-compact 처럼 HUD 를 감싸 호출하는 래퍼라면 정상 표시된다. 이 스킬은 OMC 커스텀 HUD 래퍼에만 붙는다.
66
68
  - 추정치가 실제 "Request too large" 시점과 크게 어긋나면 Claude Code 가 세션 파일 형식이나 요청 필터를 바꿨을 수 있다. banker 이슈로 알린다.
@@ -49,7 +49,7 @@ import {
49
49
  writeFileSync,
50
50
  } from "node:fs";
51
51
  import { homedir, tmpdir } from "node:os";
52
- import { basename, dirname, join } from "node:path";
52
+ import { basename, dirname, join, resolve } from "node:path";
53
53
  import { fileURLToPath, pathToFileURL } from "node:url";
54
54
  import { CACHE_DIR, clearCache, estimatePayloadBytes, REQUEST_LIMIT_BYTES, SHOW_FROM_BYTES } from "./payload-size.mjs";
55
55
 
@@ -62,7 +62,17 @@ const CTX_LINE = /^ {2}if \(f\.ctx\) colored\.push\(colorCtx\(f\.ctx\)\);\r?\n/m
62
62
  const INPUT_DECL = /^(?:let|const) input\b/m;
63
63
  const MB = 1024 * 1024;
64
64
 
65
- const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
65
+ // The same rules as setup-omc-hud's installer (OMC's getClaudeConfigDir): trim, expand a leading "~",
66
+ // make it absolute, drop a trailing separator. A value no shell expanded ("~/x" in quotes) still finds
67
+ // the wrapper the installer put there.
68
+ function configDir() {
69
+ const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
70
+ if (!v) return join(homedir(), ".claude");
71
+ if (v === "~") return homedir();
72
+ if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
73
+ return resolve(v);
74
+ }
75
+ const CONFIG_DIR = configDir().replace(/[\\/]+$/, "");
66
76
 
67
77
  const DEFAULTS = {
68
78
  hudFile: process.env.PAYLOAD_MON_HUD_FILE || join(CONFIG_DIR, "hud", "omc-hud-custom.mjs"),
@@ -1,73 +1,134 @@
1
1
  ---
2
2
  name: setup-omc-hud
3
- description: "Claude Code 의 상태표시줄 omc_hud 를 OS별 적용 스크립트로 설치하고, Claude Code 갱신 안내를 줄 맨 끝으로 옮김. Codex 는 OMX 의 `hud` 스킬을 씀. 'setup-omc-hud'/'omc_hud 설치'/'Claude 갱신 안내 위치' 또는 `/banker:setup` 시 사용."
3
+ description: "Claude Code 상태표시줄 omc_hud 래퍼를 스킬에 든 파일로 설치. 내려받지 않고 오프라인에서 모든 OS 같은 명령으로 동작하며, PL 칸에 쓰는 payload-mon 모듈도 켬. Codex 는 OMX 의 `hud` 스킬을 씀. 'setup-omc-hud'/'omc_hud 설치'/'상태표시줄 설치' 또는 `/banker:setup` 시 사용."
4
4
  argument-hint: "[off|status]"
5
5
  ---
6
6
 
7
- # setup-omc-hud — omc_hud 상태표시줄 설치 (OS별)
7
+ # setup-omc-hud: omc_hud 상태표시줄 설치 (오프라인)
8
8
 
9
- `smallOpenSource/omc_hud` 의 apply 스크립트를 OS에 맞게 내려받아 적용한다. `wget` 없으면 `curl`로 폴백. 답변은 한글.
9
+ 이 스킬 폴더의 `assets/` 에 든 파일로 상태표시줄 래퍼 v4 를 설치한다. 답변은 한글.
10
10
 
11
- **런타임:** omc_hud 는 Claude Code 상태표시줄(`~/.claude/settings.json` statusLine) 대상이다. **Codex 런타임에선 oh-my-codex(OMX)가 자체 `hud` 스킬/구성을 제공**하므로, Codex에선 이 스킬 대신 OMX 의 `hud` 를 사용한다.
11
+ - 내려받는 것이 없다. 네트워크가 없어도 끝까지 된다.
12
+ - Windows, Linux, macOS 모두 `node` 명령 하나로 같은 절차를 밟는다.
13
+ - 설치 파일: `apply-hud.mjs`(설치 스크립트), `omc-hud-custom.mjs`(래퍼 v4), `hud-config.json`(HUD 설정), `LICENSE-omc_hud`(원본 omc_hud 의 MIT 라이선스)
12
14
 
13
- **인자가 `off` 또는 `status` 일 때:** 0~3단계를 건너뛰고 3단계의 Claude 갱신 안내 스크립트만 그 인자로 실행해 결과를 보고한다(`off` 는 안내를 래퍼가 정한 자리로 되돌림, `status` 는 상태만 확인).
15
+ **런타임:** Claude Code 상태표시줄(`<설정 폴더>/settings.json` 의 `statusLine`)이 대상이다. Codex 런타임에서는 oh-my-codex(OMX)가 자체 `hud` 스킬을 주므로 이 스킬 대신 OMX 의 `hud` 를 쓴다.
16
+
17
+ **인자가 `off` 또는 `status` 일 때:** 0~3단계를 건너뛰고 "인자 off, status" 절만 실행한다.
18
+
19
+ ## 표시 형식
20
+
21
+ ```
22
+ <model>/<effort>|5h:<pct>/<reset>|wk:<pct>/<reset>|<mm>:<pct>/<reset>|think|PL:<mb>M|se:<hours>h|ctx:<pct>%|🤖<agents>|🔧<tools>|⚡<skills>|<cwd>|<account>|<branch>|claude<cur>→<new>
23
+ <에이전트 목록, 경고 같은 상세 줄>
24
+ ```
25
+
26
+ | 변수 | 뜻 | 예 | 나오는 조건 |
27
+ |---|---|---|---|
28
+ | `<model>` | 모델 이름 앞 2글자와 버전 | `Op5.5` | 항상 |
29
+ | `<effort>` | 추론 강도 | `high` | Claude Code 가 넘기거나 OMC 가 effort 칸을 낼 때 |
30
+ | `<pct>`, `<reset>` | 사용률과 초기화까지 남은 시간 | `96%`, `1h5m` | 사용량 정보가 있을 때 |
31
+ | `<mm>` | 모델별 사용량의 모델 이름 앞 2글자 | `Fa` | OMC 가 모델별 사용량을 낼 때 |
32
+ | `think` | 확장 사고 표시 | `think` | 확장 사고가 켜져 있을 때 |
33
+ | `<mb>` | 요청 크기 추정치(MB, 소수 1자리) | `0.5` | payload-mon 모듈이 있을 때 |
34
+ | `<hours>` | 세션 시간(시간, 소수 1자리) | `0.7` | 항상 |
35
+ | `ctx:<pct>%` | 대화 맥락 사용률 | `ctx:12%` | OMC 가 ctx 를 낼 때 |
36
+ | `<agents>` | 이 세션의 에이전트 호출 수 | `1` | 1 이상일 때 |
37
+ | `<tools>` | 도구 호출 수 | `14` | 1 이상일 때 |
38
+ | `<skills>` | 스킬 호출 수 | `3` | 1 이상일 때 |
39
+ | `<cwd>` | 작업 폴더 | `/home/user/project` | 항상 |
40
+ | `<account>` | 로그인 계정 이메일의 `@` 앞부분 | `user` | `.claude.json` 에 이메일이 있을 때 |
41
+ | `<branch>` | git 브랜치 | `main` | git 저장소 안일 때 |
42
+ | `<cur>`, `<new>` | 지금 Claude Code 판과 새 판 | `2.1.300`, `2.1.302` | 새 판이 있을 때. 지금 판을 모르면 `claude→<new>` |
43
+
44
+ - 조건이 맞지 않는 칸은 구분자 `|` 와 함께 빠진다.
45
+ - 색
46
+ - `%` 는 70% 미만 초록, 85% 미만 노랑, 그 이상 분홍(256색 213)
47
+ - `<mb>` 는 32MB 대비 같은 기준
48
+ - OMC 는 ctx 가 80% 를 넘으면 뒤에 `COMPRESS?`, `CRITICAL` 을 붙인다. 래퍼는 이 글자를 빼고 색으로만 보인다.
49
+ - ralph, autopilot, `agents:<n>`(실행 중인 에이전트) 같은 OMC 상태 칸은 활성일 때만 `⚡<skills>` 와 `<cwd>` 사이에 흐리게 나온다.
50
+ - 화면 폭을 넘으면 칸 사이에서 다음 줄로 넘어간다.
51
+ - 둘째 줄부터는 OMC 의 상세 줄(에이전트 목록, todo, 경고)이다. OMC 와 Claude Code 의 갱신 안내 줄은 뺀다.
52
+ - 첨부 HUD 설정은 OMC 출력을 8줄까지 둔다. 넘으면 OMC 가 상세 줄을 `... (+N lines)` 한 줄로 접는다.
14
53
 
15
54
  ## 0. 감지
55
+
56
+ - `node -v` 로 Node 18 이상인지 본다. OMC 5.7.4 는 Node 20 이상을 적는다.
57
+ - 설정 폴더는 `$CLAUDE_CONFIG_DIR`, 없으면 `~/.claude` 다.
58
+ - OMC 가 설치돼 있어야 한다. 없으면 1단계가 멈추고 알린다.
59
+
60
+ ## 1. 적용
61
+
62
+ 모든 OS 에서 같은 명령이다.
63
+
64
+ bash, zsh:
16
65
  ```bash
17
- case "$(uname -s)" in Darwin) OS=mac;; Linux) OS=linux;; *) OS=windows;; esac; echo "OS=$OS"
18
- command -v wget >/dev/null && echo "dl=wget" || { command -v curl >/dev/null && echo "dl=curl" || echo "dl=none(설치 필요)"; }
19
- # 적용 스크립트가 래퍼를 덮어쓰면 payload-mon 표시 블록이 빠진다. 지금 켜져 있는지 기억해 둔다(3단계에서 사용).
20
- HUD_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hud/omc-hud-custom.mjs"
21
- grep -qF '// >>> payload-mon >>>' "$HUD_FILE" 2>/dev/null && echo "payload-mon=on" || echo "payload-mon=off"
66
+ node "<이 스킬 디렉터리 절대경로>/assets/apply-hud.mjs"
22
67
  ```
23
- Windows(PowerShell)에서는 같은 확인을 이렇게 한다:
68
+
69
+ PowerShell:
24
70
  ```powershell
25
- $cfg = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { "$env:USERPROFILE\.claude" }
26
- if (Select-String -Quiet -SimpleMatch -LiteralPath "$cfg\hud\omc-hud-custom.mjs" -Pattern '// >>> payload-mon >>>' -ErrorAction SilentlyContinue) { "payload-mon=on" } else { "payload-mon=off" }
71
+ node "<이 스킬 디렉터리 절대경로>\assets\apply-hud.mjs"
27
72
  ```
28
73
 
29
- ## 1. mac / linux — apply 스크립트 적용
74
+ - `[ok] statusLine -> node "<설정 폴더>/hud/omc-hud-custom.mjs"` 줄을 확인한다.
75
+ - 기존 statusLine 이 다른 명령이었으면 `[..] statusLine ran ...` 줄이 나온다. 보고에 그대로 넣는다.
76
+ - 종료 코드 1 이면 출력의 `[x]` 줄을 그대로 보고하고 멈춘다. 이때 바뀐 파일은 없다.
77
+ - `settings.json` 이 JSON 객체가 아니면 사용자가 고친 뒤 다시 실행한다.
78
+ - OMC 가 없으면 OMC 를 먼저 설치한다(`setup-omc` 스킬). 이 스킬은 OMC 를 설치하지 않는다.
79
+ - 설정 폴더 경로에 셸이 해석하는 글자가 있으면 그 글자가 없는 폴더를 쓰게 안내한다.
80
+ - 쓸 수 없는 파일이나 폴더가 있으면 권한을 고친 뒤 다시 실행한다.
81
+ - 종료 코드 2 면 예기치 못한 오류로 중간에 멈춘 것이다. 출력을 그대로 보고하고, 바뀐 파일의 이전 판은 `<파일>.backup.<시각>` 에 있다고 알린다.
82
+
83
+ ## 2. payload-mon 켜기
84
+
85
+ 이전 상태와 상관없이 늘 실행한다.
86
+
30
87
  ```bash
31
- RAW=https://raw.githubusercontent.com/smallOpenSource/omc_hud/refs/heads/main
32
- mkdir -p ~/dummy && cd ~/dummy
33
- script=$([ "$OS" = mac ] && echo apply-hud-macos.sh || echo apply-hud-linux.sh)
34
- for f in "$script" apply-hud.mjs hud-config.json omc-hud-custom.mjs; do
35
- if command -v wget >/dev/null; then wget -q "$RAW/$f"; else curl -fsSLO "$RAW/$f"; fi
36
- done
37
- chmod +x "$script" && "./$script"
38
- rm -f "$script" apply-hud.mjs hud-config.json omc-hud-custom.mjs # 정리
39
- cd - >/dev/null
88
+ node "<이 스킬 디렉터리 절대경로>/../payload-mon/scripts/payload-mon.mjs" on
40
89
  ```
41
90
 
42
- ## 2. windows
43
- `wget`이 없는 경우가 많아 **프로젝트 통째로 받아 적용**한다:
44
- ```powershell
45
- # git 있으면
46
- git clone https://github.com/smallOpenSource/omc_hud.git "$env:TEMP\omc_hud"; cd "$env:TEMP\omc_hud"
47
- # 저장소의 windows 적용 스크립트(apply-hud-*.ps1/.cmd) 또는 `node apply-hud.mjs` 안내를 따른다.
91
+ - Codex 사본에서 실행할 때 경로는 `<이 스킬 디렉터리 절대경로>/../banker-payload-mon/scripts/payload-mon.mjs` 다.
92
+ - 이유: `PL:` 칸은 payload-mon 이 래퍼 옆(`hud/payload-mon/`)에 두는 크기 계산 모듈을 쓴다. 모듈이 없으면 `PL:` 이 나오지 않는다.
93
+ - 이 단계의 출력 "8MB 이상이면 ctx 옆에 payload가 표시됩니다" 는 옛 래퍼 기준이다. 그대로 옮기지 말고 "PL 칸에 쓰는 모듈을 설치함" 으로 보고한다.
94
+
95
+ ## 3. 보고
96
+
97
+ - 1단계 출력의 statusLine 줄과 백업 경로
98
+ - 2단계 결과
99
+ - statusLine 이 바뀌었으면 Claude Code 재시작 안내
100
+
101
+ ## 인자 off, status
102
+
103
+ 옛 래퍼(v3)에 넣었던 Claude 갱신 안내 블록(`claude-update-last`)을 다룬다. v4 래퍼는 갱신 안내를 직접 줄 맨 끝에 두므로 새 설치에는 이 블록이 필요 없다.
104
+
105
+ ```bash
106
+ node "<이 스킬 디렉터리 절대경로>/scripts/claude-update-last.mjs" off
48
107
  ```
49
- git 없으면 GitHub "Download ZIP" → 해제 → 동봉 적용 스크립트 실행.
50
-
51
- ## 3. 검증·보고
52
- - 적용 스크립트가 `~/.claude/settings.json` 의 statusLine 등을 갱신했는지(스크립트 출력 확인).
53
- - HUD는 **다음 프롬프트/세션**부터 반영될 수 있음 → 안 보이면 재시작 안내.
54
- - `~/dummy` 임시파일 정리 확인. 이미 적용돼 있으면 재적용 불필요.
55
- - **payload-mon 복구**: 0단계에서 `payload-mon=on` 이었다면, 적용 스크립트가 래퍼를 덮어써 payload 표시 블록이 빠진 상태다.
56
- payload-mon 스킬(Claude Code `/banker:payload-mon`, Codex `banker-payload-mon`)을 `on` 으로 실행해 다시 켜고, 그 결과를 함께 보고한다.
57
- `payload-mon=off` 였다면 아무것도 하지 않는다(사용자가 켠 적 없는 기능을 켜지 않는다).
58
- - **Claude 갱신 안내 위치**: payload-mon 처리 뒤에 이 스킬의 스크립트를 `on` 으로 실행한다. 재적용을 건너뛴 경우에도 실행하고, Windows 도 같은 명령이다.
59
- ```bash
60
- node "<이 스킬 디렉터리 절대경로>/scripts/claude-update-last.mjs" on
61
- ```
62
- 새 Claude Code 가 나오면 OMC 가 띄우는 `[Claude#2.1.288] -> 2.1.289 claude update` 를 상태표시줄 1번째 줄 맨 끝(OMC 표시 뒤)으로 옮긴다. 2번째 줄의 `[!] claude 2.1.289 - paste: ! claude update` 안내는 그대로 둔다.
63
- 출력을 그대로 보고한다. 종료 코드 1(거부)이면 래퍼는 바뀌지 않았으니 이유만 보고하고, 2(예기치 못한 오류로 중간에 멈춤)면 `status` 결과를 함께 보고한다.
64
- 되돌리기는 `off`, 상태 확인은 `status`.
108
+
109
+ - `off` 는 블록을 뺀다. `status` 는 상태만 본다.
110
+ - 출력을 그대로 보고한다. 종료 코드 1(거부)이면 래퍼는 바뀌지 않았으니 이유만 보고한다.
111
+ - 종료 코드 2(예기치 못한 오류로 중간에 멈춤)면 `status` 결과를 함께 보고한다.
65
112
 
66
113
  ## 함정
67
- - `curl` 다운로드는 **대문자 `-O`**(원격 파일명 유지). 소문자 `-o`는 출력명 지정이라 다름.
68
- - raw URL 의 `refs/heads/main` 브랜치명이 바뀌면 404 → 저장소 확인.
69
- - `.mjs` 적용은 Node 필요(`node -v`).
70
- - `claude-update-last` 를 `off` 로 꺼 두었어도 이 스킬을 다시 실행하면 3단계에서 다시 켜진다. 래퍼가 정한 자리를 원하면 끝난 뒤 `off`.
71
- - 블록은 OMC 의 안내 문구(`[Claude#X] -> Y claude update`)로 안내를 찾는다. OMC 가 문구를 바꾸면 안내는 래퍼가 정한 자리에 남고, 상태표시줄은 깨지지 않는다.
72
- - 블록은 래퍼가 세그먼트를 합치는 줄에서 `segs`·`colored` 를 읽는다. 래퍼 구조가 바뀌어 그 줄에서 이 이름이 보이지 않으면 `on` 은 성공해도 효과가 없다(상태표시줄은 그대로). 새 Claude Code 가 나왔을 때 안내가 맨 끝에 오는지로 확인한다.
73
- - 이 블록은 플러그인 밖(`~/.claude/hud/`)에 남는다. 플러그인을 지우기 전에 `off` 를 실행한다.
114
+
115
+ - 다시 설치하면 래퍼와 `hud-config.json` 을 첨부 파일로 덮어쓴다. 손으로 고친 내용은 `.backup.<시각>` 에만 남는다.
116
+ - `settings.json` 의 `omcHud` 값은 `hud-config.json` 의 같은 키를 덮어쓴다. 래퍼가 기대는 아래 키를 바꾸면 칸이 달라지거나 OMC 원문이 그대로 나온다.
117
+ - `omcLabel`, `useBars`
118
+ - `maxWidth`, `maxOutputLines`
119
+ - `showCallCounts`, `callCountsFormat`
120
+ - `contextBar`, `gitBranch`
121
+ - `payload-mon off` 를 실행하면 크기 계산 모듈이 지워져 `PL:` 칸도 사라진다. 다시 보려면 `payload-mon on`.
122
+ - v4 에서 payload-mon 블록은 모듈을 깔기 위해서만 남는다. 갱신마다 크기 추정이 두 번 돌지만 두 번째는 캐시를 쓴다.
123
+ - 래퍼를 손으로 고칠 때 `if (f.ctx) colored.push(colorCtx(f.ctx));` 줄을 지우지 않는다. 실행상 비어 있지만 `payload-mon on` 이 이 줄 바로 뒤에 블록을 넣는다.
124
+ - 래퍼는 statusLine 프로세스가 받은 터미널 폭(stderr, stdout 의 `columns`, 그다음 `COLUMNS`)으로 줄을 접는다. 폭을 받지 못하면 한 줄로 나오고 Claude Code 가 끝을 자른다.
125
+ - Claude Code 는 자기 화면이 터미널이면 그 폭과 높이를 `COLUMNS`, `LINES` 로 넘긴다(2.1.303 에서 확인). 그래서 보통은 접힌다.
126
+ - 분홍은 256색 코드다. 256색을 못 쓰는 터미널에서는 기본 색으로 보인다.
127
+ - statusLine 은 PATH 의 `node` 를 쓴다. GUI 로 띄운 Claude Code(macOS Dock, VS Code 확장)는 PATH 에 `/opt/homebrew/bin` 같은 폴더가 빠질 수 있다(추정). 상태표시줄이 안 나오면 PATH 를 확인한다.
128
+ - 설정 폴더 경로에 `"`, 백틱, `$`, `%`, `\`, 줄바꿈이 있으면 설치 스크립트가 거부한다. statusLine 명령의 큰따옴표 안에서도 셸이 이 글자를 해석해, 명령이 깨지거나 경로 속 명령이 실행되기 때문이다.
129
+ - 기존 statusLine 의 다른 필드(`padding`, `refreshInterval` 등)는 그대로 둔다. 다른 래퍼(smart-compact 등)가 감싼 명령은 이 래퍼의 명령으로 바뀐다. 이전 값은 `settings.json` 백업에 있다.
130
+ - 갱신 안내는 OMC 의 문구 `[Claude#<cur>] -> <new> claude update` 로 찾는다. OMC 가 문구를 바꾸면 줄이지 못하고 원문이 경로 앞 칸에 남는다. 상태표시줄은 깨지지 않는다.
131
+ - `[!] omc <ver> - paste: ` 와 `[!] claude <new> - paste: ` 로 시작하는 갱신 안내 줄은 뺀다. OMC 가 이 문구를 바꾸면 다시 보인다.
132
+ - 설치는 네트워크를 쓰지 않는다. 다만 OMC 가 렌더할 때 네트워크로 얻는 정보(새 Claude Code 판 같은)는 오프라인에서 빠질 수 있다.
133
+ - 래퍼, HUD 설정, 모듈 사본은 플러그인 밖(`<설정 폴더>/hud/`, `<설정 폴더>/.omc/`)에 있다. 플러그인을 지워도 상태표시줄은 남는다.
134
+ - OMC 를 지울 때는 statusLine 도 지운다. OMC 로더는 플러그인을 못 찾으면 작업 폴더의 `node_modules` 에서 OMC HUD 를 찾아 불러오기 때문이다.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 smallOpenSource
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * apply-hud.mjs — cross-platform installer for the custom OMC HUD statusline.
4
+ *
5
+ * Installs into ${CLAUDE_CONFIG_DIR:-~/.claude}:
6
+ * - hud/omc-hud.mjs (OMC loader, copied from the installed plugin)
7
+ * - hud/lib/config-dir.mjs (loader dependency)
8
+ * - hud/omc-hud-custom.mjs (the custom compact/colored formatter)
9
+ * - .omc/hud-config.json (HUD preset/elements)
10
+ * - settings.json (statusLine -> the custom formatter; existing file backed up)
11
+ *
12
+ * Runs identically on Linux, macOS, and Windows (only needs node + an OMC install).
13
+ * Run it directly with `node apply-hud.mjs` on every OS; the templates are read
14
+ * from this file's folder, so nothing is downloaded.
15
+ *
16
+ * Based on smallOpenSource/omc_hud d3cedd5 (MIT, see LICENSE-omc_hud). Changes:
17
+ * statusLine path quoted; a settings.json that is not a JSON object is refused
18
+ * before any file changes (the original replaced it); UTF-8 BOM accepted;
19
+ * CLAUDE_CONFIG_DIR trimmed, "~" expanded and made absolute; formatter and HUD
20
+ * config backed up when they differ; pre-releases sort below their release; no
21
+ * folder is made when OMC is missing. Also refused before any change: a config
22
+ * folder path the statusLine shell would act on, a file or folder it cannot
23
+ * write, an OMC folder without both loader files, a relative OMC_PLUGIN_ROOT.
24
+ * Backups keep the original bytes and mode; the files it writes are 0644 and new
25
+ * folders 0755; the other statusLine fields (padding, ...) stay.
26
+ * Exit 1 means nothing changed; exit 2 means an unexpected error stopped it partway.
27
+ */
28
+ import {
29
+ accessSync,
30
+ chmodSync,
31
+ constants,
32
+ copyFileSync,
33
+ existsSync,
34
+ mkdirSync,
35
+ readdirSync,
36
+ readFileSync,
37
+ writeFileSync,
38
+ } from "node:fs";
39
+ import { homedir } from "node:os";
40
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
41
+ import { fileURLToPath } from "node:url";
42
+
43
+ const SELF = dirname(fileURLToPath(import.meta.url));
44
+ // Same rules as OMC's getClaudeConfigDir: trim, expand a leading "~", then make
45
+ // it absolute so statusLine works from any cwd.
46
+ function configDir() {
47
+ const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
48
+ if (!v) return join(homedir(), ".claude");
49
+ if (v === "~") return homedir();
50
+ if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
51
+ return resolve(v);
52
+ }
53
+ const CONFIG_DIR = configDir().replace(/[\\/]+$/, "");
54
+ const HUD_DIR = join(CONFIG_DIR, "hud");
55
+ const OMC_DIR = join(CONFIG_DIR, ".omc");
56
+ const settingsPath = join(CONFIG_DIR, "settings.json");
57
+ const loaderDst = join(HUD_DIR, "omc-hud.mjs");
58
+ const libDst = join(HUD_DIR, "lib", "config-dir.mjs");
59
+ const wrapperDst = join(HUD_DIR, "omc-hud-custom.mjs");
60
+ const configDst = join(OMC_DIR, "hud-config.json");
61
+ // An absolute forward-slash path: Claude Code runs statusLine through a shell (Git Bash or
62
+ // PowerShell on Windows) that treats backslashes as escapes.
63
+ const cmdPath = wrapperDst.split(sep).join("/");
64
+ const log = (m) => process.stdout.write(m + "\n");
65
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
66
+ const refuse = (lines) => {
67
+ for (const l of lines) log(l);
68
+ process.exit(1);
69
+ };
70
+
71
+ // 0. Check settings.json before touching anything, so a refusal leaves every
72
+ // file as it was. Refuse rather than replace: a fresh file would drop every
73
+ // other setting. A BOM (Windows editors) is dropped when the file is written
74
+ // back; the backup keeps the original bytes.
75
+ let settings = {};
76
+ let settingsRaw = null;
77
+ if (existsSync(settingsPath)) {
78
+ settingsRaw = readFileSync(settingsPath, "utf8");
79
+ try {
80
+ settings = JSON.parse(settingsRaw.replace(/^\uFEFF/, ""));
81
+ } catch {
82
+ settings = undefined;
83
+ }
84
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
85
+ refuse(["[x] Existing settings.json is not a JSON object; nothing changed. Fix it and re-run."]);
86
+ }
87
+ }
88
+
89
+ // Inside the command's double quotes these still act: a quote ends the path, $ and `
90
+ // run what follows, %VAR% expands, a backslash escapes, a line break ends the command.
91
+ if (/["`$%\\\r\n]/.test(cmdPath)) {
92
+ refuse([
93
+ '[x] The config folder path has a character the statusLine shell would act on (" ` $ % \\ or a line break); nothing changed.',
94
+ " Use a config folder without it (CLAUDE_CONFIG_DIR).",
95
+ ]);
96
+ }
97
+
98
+ // 1. Resolve the installed OMC plugin root (for the canonical HUD loader template).
99
+ function resolvePluginRoot() {
100
+ const lib = (r) => join(r, "scripts", "lib");
101
+ // The loader needs both files; a folder holding only one would leave hud/ half replaced.
102
+ const complete = (r) => existsSync(join(lib(r), "hud-wrapper-template.txt")) && existsSync(join(lib(r), "config-dir.mjs"));
103
+ const fromEnv = process.env.OMC_PLUGIN_ROOT;
104
+ // Only an absolute path: a relative one would be read from the project folder the command runs in.
105
+ if (fromEnv && isAbsolute(fromEnv) && complete(fromEnv)) return fromEnv;
106
+ const base = join(CONFIG_DIR, "plugins", "cache", "omc", "oh-my-claudecode");
107
+ if (!existsSync(base)) return null;
108
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([A-Za-z0-9.-]+))?$/;
109
+ // Newest first; a pre-release sorts below its release (5.7.4-beta < 5.7.4).
110
+ const newer = (a, b) => {
111
+ const x = a.match(SEMVER);
112
+ const y = b.match(SEMVER);
113
+ for (let i = 1; i <= 3; i++) if (+x[i] !== +y[i]) return +y[i] - +x[i];
114
+ if (!x[4] !== !y[4]) return x[4] ? 1 : -1;
115
+ return (y[4] || "").localeCompare(x[4] || "", undefined, { numeric: true });
116
+ };
117
+ const versions = readdirSync(base)
118
+ .filter((n) => SEMVER.test(n))
119
+ .sort(newer);
120
+ for (const v of versions) {
121
+ const r = join(base, v);
122
+ if (complete(r)) return r;
123
+ }
124
+ return null;
125
+ }
126
+
127
+ const pluginRoot = resolvePluginRoot();
128
+ if (!pluginRoot && !existsSync(loaderDst)) {
129
+ refuse([
130
+ "[x] OMC plugin not installed and no existing hud/omc-hud.mjs found.",
131
+ " Install OMC first, then re-run:",
132
+ " /plugin install oh-my-claudecode (or set OMC_PLUGIN_ROOT to an OMC version folder)",
133
+ ]);
134
+ }
135
+
136
+ // Every file this run writes or backs up, checked before the first write so that a
137
+ // refusal changes nothing; a new file or backup needs its folder, or the nearest one
138
+ // that exists, to take it.
139
+ function unwritable() {
140
+ for (const p of [settingsPath, loaderDst, libDst, wrapperDst, configDst]) {
141
+ if (!existsSync(p)) continue;
142
+ try {
143
+ accessSync(p, constants.W_OK);
144
+ } catch (e) {
145
+ return `${p} (${e.code})`;
146
+ }
147
+ }
148
+ for (const dir of [CONFIG_DIR, HUD_DIR, join(HUD_DIR, "lib"), OMC_DIR]) {
149
+ let d = dir;
150
+ while (!existsSync(d) && dirname(d) !== d) d = dirname(d);
151
+ try {
152
+ accessSync(d, constants.W_OK);
153
+ } catch (e) {
154
+ return `${d} (${e.code})`;
155
+ }
156
+ }
157
+ return "";
158
+ }
159
+ const blocked = unwritable();
160
+ if (blocked) refuse([`[x] Cannot write ${blocked}; nothing changed. Fix the permission and re-run.`]);
161
+
162
+ // The statusline runs or reads these on every refresh, so no other account may change them.
163
+ const FILE_MODE = 0o644;
164
+ function writeOwned(file, data) {
165
+ writeFileSync(file, data, { mode: FILE_MODE });
166
+ if (process.platform !== "win32") chmodSync(file, FILE_MODE);
167
+ }
168
+
169
+ // Keep a timestamped copy, with its bytes and mode, of a file that differs from what replaces it.
170
+ function backupIfDifferent(dst, next, label) {
171
+ if (!existsSync(dst)) return;
172
+ if (readFileSync(dst).equals(next)) return;
173
+ const keep = dst + ".backup." + stamp;
174
+ copyFileSync(dst, keep);
175
+ log(`[ok] Backed up ${label} -> ${keep}`);
176
+ }
177
+
178
+ // 2. Ensure target directories exist, then copy the loader.
179
+ function installLoader() {
180
+ for (const d of [join(HUD_DIR, "lib"), OMC_DIR]) mkdirSync(d, { recursive: true, mode: 0o755 });
181
+ if (!pluginRoot) {
182
+ log("[..] OMC plugin root not found; keeping existing hud/omc-hud.mjs");
183
+ return;
184
+ }
185
+ writeOwned(loaderDst, readFileSync(join(pluginRoot, "scripts", "lib", "hud-wrapper-template.txt")));
186
+ writeOwned(libDst, readFileSync(join(pluginRoot, "scripts", "lib", "config-dir.mjs")));
187
+ log("[ok] OMC HUD loader installed from " + pluginRoot);
188
+ }
189
+
190
+ // 3. Install the custom formatter and HUD config from the bundled templates.
191
+ // Files that differ from the templates (local edits, blocks added by
192
+ // payload-mon / claude-update-last) are kept as timestamped backups first.
193
+ function installTemplates() {
194
+ const wrapperSrc = readFileSync(join(SELF, "omc-hud-custom.mjs"));
195
+ const configSrc = readFileSync(join(SELF, "hud-config.json"));
196
+ backupIfDifferent(wrapperDst, wrapperSrc, "previous formatter");
197
+ backupIfDifferent(configDst, configSrc, "previous HUD config");
198
+ writeOwned(wrapperDst, wrapperSrc);
199
+ writeOwned(configDst, configSrc);
200
+ log("[ok] Custom formatter -> " + wrapperDst);
201
+ log("[ok] HUD config -> " + configDst);
202
+ }
203
+
204
+ // 4. Point settings.json statusLine at the custom formatter; its other fields (padding, ...) stay.
205
+ const objectOr = (v) => (v && typeof v === "object" && !Array.isArray(v) ? v : {});
206
+ function pointStatusLine() {
207
+ if (settingsRaw !== null) {
208
+ const backup = settingsPath + ".backup." + stamp;
209
+ copyFileSync(settingsPath, backup); // the original bytes and mode
210
+ log("[ok] Backed up settings.json -> " + backup);
211
+ }
212
+ // Quoted so a config dir with spaces (C:/Users/First Last/.claude) still runs.
213
+ const command = `node "${cmdPath}"`;
214
+ const prior = objectOr(settings.statusLine);
215
+ if (typeof prior.command === "string" && prior.command && prior.command !== command) {
216
+ log(`[..] statusLine ran ${JSON.stringify(prior.command)}; it now runs the custom HUD (the old value is in the backup)`);
217
+ }
218
+ settings.statusLine = { ...prior, type: "command", command };
219
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
220
+ log("[ok] statusLine -> " + command);
221
+ }
222
+
223
+ try {
224
+ installLoader();
225
+ installTemplates();
226
+ pointStatusLine();
227
+ } catch (e) {
228
+ log(`[x] Stopped partway (${e.code || e.message}); some files may have changed.`);
229
+ log(` A replaced file's earlier version is in <file>.backup.${stamp}.`);
230
+ process.exit(2);
231
+ }
232
+
233
+ log("");
234
+ log("Done. Restart Claude Code to load the new statusline.");
@@ -0,0 +1,53 @@
1
+ {
2
+ "preset": "full",
3
+ "labels": {
4
+ "thinking": "think"
5
+ },
6
+ "elements": {
7
+ "omcLabel": true,
8
+ "updateNotification": true,
9
+ "model": true,
10
+ "modelFormat": "versioned",
11
+ "cwd": true,
12
+ "cwdFormat": "absolute",
13
+ "useHyperlinks": false,
14
+ "gitRepo": false,
15
+ "gitBranch": true,
16
+ "gitStatus": false,
17
+ "ralph": true,
18
+ "autopilot": true,
19
+ "prdStory": true,
20
+ "activeSkills": false,
21
+ "lastSkill": false,
22
+ "contextBar": true,
23
+ "agents": true,
24
+ "agentsFormat": "multiline",
25
+ "backgroundTasks": true,
26
+ "todos": true,
27
+ "thinking": true,
28
+ "thinkingFormat": "text",
29
+ "permissionStatus": false,
30
+ "apiKeySource": false,
31
+ "profile": false,
32
+ "promptTime": false,
33
+ "sessionHealth": true,
34
+ "showSessionDuration": true,
35
+ "showHealthIndicator": false,
36
+ "useBars": false,
37
+ "showCallCounts": true,
38
+ "callCountsFormat": "emoji",
39
+ "safeMode": true,
40
+ "maxOutputLines": 8
41
+ },
42
+ "thresholds": {
43
+ "contextWarning": 70,
44
+ "contextCompactSuggestion": 80,
45
+ "contextCritical": 85,
46
+ "ralphWarning": 7
47
+ },
48
+ "staleTaskThresholdMinutes": 30,
49
+ "contextLimitWarning": {
50
+ "threshold": 80,
51
+ "autoCompact": false
52
+ }
53
+ }
@@ -0,0 +1,545 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OMC HUD custom formatter (wrapper) — v4
4
+ *
5
+ * Runs the canonical OMC HUD (omc-hud.mjs), then reformats its rendered line
6
+ * into a compact pipe-delimited layout requested by the user:
7
+ *
8
+ * <model>/<effort>|5h:<pct>/<reset>|wk:<pct>/<reset>|<mm>:<pct>/<reset>|think|PL:<mb>M|se:<hours>h|ctx:<pct>%|🤖<agents>|🔧<tools>|⚡<skills>|<cwd>|<account>|<branch>|claude<cur>→<new>
9
+ * e.g. Op5.5/high|5h:8%/1h23m|wk:96%/1h23m|Fa:0%/1h23m|think|PL:0.4M|se:0.4h|ctx:12%|🤖1|🔧14|⚡3|...
10
+ *
11
+ * Changes vs v3:
12
+ * - OMC 5.x wraps at the terminal width; the width is hidden from OMC and the
13
+ * wrapper wraps the compact line itself at segment boundaries (no "…").
14
+ * - effort:<level> badge dropped; session -> se:<hours, 1 decimal>h.
15
+ * - ctx:<pct>% comes right after se, without OMC's " COMPRESS?" / " CRITICAL"
16
+ * words (the color shows the level).
17
+ * - OMC's call counts ("🔧14 🤖1 ⚡3") are shown agents, tools, skills, one
18
+ * segment each: 🤖1|🔧14|⚡3. A count OMC leaves out (zero) stays out.
19
+ * - per-model usage badge (fable:0%(1h31m)) -> Fa:0%/1h31m, after the rate windows.
20
+ * - PL:<MB, 1 decimal>M always shown before se. It reads payload-mon's module
21
+ * copy next to this file (hud/payload-mon/payload-size.mjs); without it PL
22
+ * is left out.
23
+ * - cwd group split on "|"; branch shown without "branch:" after the account.
24
+ * - OMC#X.Y.Z label no longer shown, nor OMC's update hint lines
25
+ * ("[!] omc X - paste: ! ...", "[!] claude X - paste: ! claude update").
26
+ * - Claude Code update notice "[Claude#2.1.300] -> 2.1.302 claude update" ->
27
+ * "claude2.1.300→2.1.302", shown last. A claude-update-last block has
28
+ * nothing left to move.
29
+ * - >=85% is pink (256-color 213) instead of red.
30
+ * - Blocks added by `payload-mon on` / `claude-update-last on` are not part of
31
+ * this file; those tools insert them.
32
+ *
33
+ * Why a wrapper: OMC's render.js hardcodes " | " separators, the "[...]" label
34
+ * brackets, the "Model: " prefix, the "percent(time)" rate layout and the
35
+ * "session:Nm" label. None are configurable, so we transform the final text.
36
+ * All data (usage %, reset times, tool counts, cwd) still comes from OMC.
37
+ *
38
+ * Defensive: on any parse failure it prints OMC's raw output unchanged, so the
39
+ * statusline never breaks if OMC's format changes after an update.
40
+ *
41
+ * ─────────────────────────────────────────────────────────────────────────────
42
+ * Changes vs v1 (formatting logic below is byte-identical to v1 on purpose):
43
+ *
44
+ * 1. The canonical HUD runs IN THIS PROCESS instead of via spawnSync, so a
45
+ * statusline refresh costs one node process instead of two. Windows charges
46
+ * kernel paged pool per process creation (fltmgr FMfn name-cache node), and
47
+ * on this machine that accumulation is not reclaimable short of a reboot.
48
+ *
49
+ * 2. Every unbounded wait now has a time budget. v1 could block forever in two
50
+ * places with no timeout: readFileSync(0) waiting for stdin EOF, and
51
+ * spawnSync waiting on the child. Both are SYNCHRONOUS, so a setTimeout
52
+ * watchdog cannot fire while they block — the event loop is not running.
53
+ * A stalled instance never dies, and the next refresh spawns another, so
54
+ * they accumulate linearly. Both are now async and bounded.
55
+ *
56
+ * Timer discipline comes from a 3-OS reproduction documented in
57
+ * .omc/wiki/bounding-a-hanging-dynamic-import-in-node.md:
58
+ * - the budget timer stays REF'd: an unref'd timer never fires when Node bails
59
+ * out of an unsettled top-level await with exit 13,
60
+ * - the self-exit timer is UNREF'd: it must not hold a healthy process open,
61
+ * and when something really is hung the loop is alive so it still fires,
62
+ * - the budget is 10s, not 3s: a tight budget silently discards work that was
63
+ * merely slow, without saving any time (the hung import holds the loop anyway).
64
+ *
65
+ * 🔴 omc-hud.mjs ends with a bare `main();` — no await. Module evaluation
66
+ * therefore completes BEFORE any output is produced. Awaiting the import
67
+ * alone captures nothing (that is why v2 printed "[OMC HUD] no output").
68
+ * We wait for the first complete line instead.
69
+ */
70
+ import { spawnSync } from "node:child_process";
71
+ import { readFileSync } from "node:fs";
72
+ import { homedir } from "node:os";
73
+ import { dirname, join } from "node:path";
74
+ import { Readable } from "node:stream";
75
+ import { fileURLToPath, pathToFileURL } from "node:url";
76
+
77
+ const __dirname = dirname(fileURLToPath(import.meta.url));
78
+ const HUD = join(__dirname, "omc-hud.mjs");
79
+
80
+ const STDIN_TIMEOUT_MS = 2000; // Claude Code pipes the payload immediately
81
+ const RENDER_TIMEOUT_MS = 10000; // see timer discipline note above
82
+ const SPAWN_TIMEOUT_MS = 8000; // fallback path only
83
+ const EXIT_GRACE_MS = 2000;
84
+ const HARD_EXIT_MS = 20000;
85
+
86
+ // Last-resort guard: if any path leaves us alive past this, end voluntarily.
87
+ // unref'd so a healthy render is never held open by it.
88
+ {
89
+ const g = setTimeout(() => {
90
+ try {
91
+ process.stdout.write("\n");
92
+ } catch {
93
+ /* stdout already gone */
94
+ }
95
+ process.exit(0);
96
+ }, HARD_EXIT_MS);
97
+ if (g && g.unref) g.unref();
98
+ }
99
+
100
+ // Print and terminate. The canonical HUD ran in-process, so its own async work
101
+ // may still be pending on the event loop; once our line is out there is nothing
102
+ // left to wait for. The write callback is the flush-safe exit point, and the
103
+ // unref'd timer covers the case where the callback never arrives.
104
+ // Only the first call prints: the fallback path may emit and then fall through
105
+ // to the formatter, whose passthrough catch would print a second line.
106
+ let emitted = false;
107
+ function emit(s) {
108
+ if (emitted) return;
109
+ emitted = true;
110
+ try {
111
+ process.stdout.write(s, () => process.exit(0));
112
+ } catch {
113
+ process.exit(0);
114
+ }
115
+ const k = setTimeout(() => process.exit(0), EXIT_GRACE_MS);
116
+ if (k && k.unref) k.unref();
117
+ }
118
+
119
+ // Currently logged-in Claude account: the part of the email before "@".
120
+ // Read from <config home>/.claude.json (oauthAccount.emailAddress) via a light
121
+ // regex so we don't JSON.parse the whole config every render. Claude Code keeps
122
+ // that file in $CLAUDE_CONFIG_DIR when set, otherwise in the home folder.
123
+ function claudeJsonDir() {
124
+ const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
125
+ if (!v) return homedir();
126
+ if (v === "~") return homedir();
127
+ if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
128
+ return v;
129
+ }
130
+ function loginAccount() {
131
+ try {
132
+ const raw = readFileSync(join(claudeJsonDir(), ".claude.json"), "utf8");
133
+ const m = raw.match(/"emailAddress"\s*:\s*"([^"@]+)@/);
134
+ return m ? m[1] : "";
135
+ } catch {
136
+ return "";
137
+ }
138
+ }
139
+
140
+ // Read the statusline JSON Claude Code pipes on stdin, bounded.
141
+ // A TTY means a human ran this by hand — there is no EOF coming, so skip it
142
+ // entirely rather than hang waiting for one.
143
+ function readStdinBounded(ms) {
144
+ if (process.stdin.isTTY) return Promise.resolve("");
145
+ return new Promise((resolve) => {
146
+ let data = "";
147
+ let done = false;
148
+ const onData = (c) => {
149
+ data += c;
150
+ };
151
+ const finish = () => {
152
+ if (done) return;
153
+ done = true;
154
+ clearTimeout(t);
155
+ process.stdin.removeListener("data", onData);
156
+ process.stdin.removeListener("end", finish);
157
+ process.stdin.removeListener("error", finish);
158
+ try {
159
+ process.stdin.pause();
160
+ } catch {
161
+ /* already closed */
162
+ }
163
+ resolve(data);
164
+ };
165
+ const t = setTimeout(finish, ms); // ref'd on purpose
166
+ try {
167
+ process.stdin.setEncoding("utf8");
168
+ process.stdin.on("data", onData);
169
+ process.stdin.once("end", finish);
170
+ process.stdin.once("error", finish);
171
+ } catch {
172
+ finish();
173
+ }
174
+ });
175
+ }
176
+
177
+ const input = await readStdinBounded(STDIN_TIMEOUT_MS);
178
+
179
+ // Current reasoning effort level (low/medium/high/xhigh) from the statusline
180
+ // payload (effort.level) — shown next to the model. Empty if absent.
181
+ let effort = "";
182
+ try {
183
+ effort = String(JSON.parse(input)?.effort?.level || "");
184
+ } catch {
185
+ /* no/invalid stdin JSON */
186
+ }
187
+
188
+ // Run the canonical HUD in-process, capturing what it writes to stdout.
189
+ // Resolves on the first complete line (see the `main();` note in the header),
190
+ // or when the budget expires — whichever comes first.
191
+ //
192
+ // 🔴 The HUD DOES read stdin — dist/hud/stdin.js readStdin() async-iterates
193
+ // process.stdin (it is not readFileSync(0)). We already drained the real fd,
194
+ // so without replaying the payload the HUD sees empty input and prints its
195
+ // "HUD installed / statusLine configured" info screen instead of a
196
+ // statusline. Swapping process.stdin for a stream carrying the same bytes
197
+ // restores identical behaviour to the spawnSync path, which fed the child
198
+ // through its stdin.
199
+ // What the HUD writes to stdout is collected instead of printed; `onLine` runs
200
+ // once a full line is in.
201
+ function captureStdout(onLine) {
202
+ const origWrite = process.stdout.write.bind(process.stdout);
203
+ let captured = "";
204
+ process.stdout.write = (chunk, enc, cb) => {
205
+ captured += typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8");
206
+ const done = typeof enc === "function" ? enc : cb;
207
+ if (typeof done === "function") done();
208
+ if (captured.includes("\n")) onLine();
209
+ return true;
210
+ };
211
+ return {
212
+ text: () => captured,
213
+ restore: () => {
214
+ process.stdout.write = origWrite;
215
+ },
216
+ };
217
+ }
218
+
219
+ async function renderInProcess(ms) {
220
+ const origStdin = Object.getOwnPropertyDescriptor(process, "stdin");
221
+ let settleFirstLine;
222
+ const firstLine = new Promise((r) => {
223
+ settleFirstLine = r;
224
+ });
225
+
226
+ const replay = Readable.from(input ? [input] : []);
227
+ replay.isTTY = false; // must stay falsy or readStdin() bails out early
228
+ try {
229
+ Object.defineProperty(process, "stdin", { value: replay, configurable: true });
230
+ } catch {
231
+ /* leave the real stdin in place; the fallback path still works */
232
+ }
233
+
234
+ const out = captureStdout(settleFirstLine);
235
+ let budgetTimer;
236
+ const budget = new Promise((r) => {
237
+ budgetTimer = setTimeout(r, ms); // ref'd on purpose
238
+ });
239
+
240
+ try {
241
+ await Promise.race([
242
+ (async () => {
243
+ await import(pathToFileURL(HUD).href);
244
+ await firstLine;
245
+ })(),
246
+ budget,
247
+ ]);
248
+ } finally {
249
+ clearTimeout(budgetTimer);
250
+ out.restore();
251
+ // Restore the real stdin so the spawnSync fallback below behaves normally.
252
+ if (origStdin) {
253
+ try {
254
+ Object.defineProperty(process, "stdin", origStdin);
255
+ } catch {
256
+ /* not restorable; we exit right after anyway */
257
+ }
258
+ }
259
+ }
260
+ return out.text();
261
+ }
262
+
263
+ // OMC 5.x wraps its line at the terminal width (index.js: stderr/stdout
264
+ // columns, then $COLUMNS). Wrapped tails land below the OMC# line, where this
265
+ // wrapper passes them through raw ("session:9m | ctx:12% | 🔧12"). The wrapper
266
+ // does its own compaction, so hide the width and let OMC emit one line.
267
+ // The width is read first: the wrapper wraps the compact line itself at the
268
+ // end, so Claude Code does not cut the tail off with "…".
269
+ const TERM_COLS =
270
+ process.stderr.columns || process.stdout.columns || parseInt(process.env.COLUMNS ?? "0", 10) || 0;
271
+ delete process.env.COLUMNS; // also keeps the spawnSync fallback unwrapped
272
+ for (const s of [process.stdout, process.stderr]) {
273
+ try {
274
+ Object.defineProperty(s, "columns", { value: undefined, configurable: true });
275
+ } catch {
276
+ /* not overridable; OMC may wrap and the tail stays below */
277
+ }
278
+ }
279
+
280
+ let raw = "";
281
+ try {
282
+ raw = (await renderInProcess(RENDER_TIMEOUT_MS)).replace(/\r/g, "");
283
+ } catch {
284
+ raw = ""; // fall through to the child-process path
285
+ }
286
+
287
+ if (!raw.trim()) {
288
+ // In-process rendering produced nothing usable. Fall back to the original
289
+ // out-of-process path: it costs an extra node, but a statusline that renders
290
+ // beats one that does not — e.g. if a future OMC version stops being
291
+ // import-safe. Bounded, unlike v1.
292
+ const res = spawnSync(process.execPath, [HUD], {
293
+ input,
294
+ encoding: "utf8",
295
+ timeout: SPAWN_TIMEOUT_MS,
296
+ killSignal: "SIGKILL",
297
+ maxBuffer: 4 * 1024 * 1024,
298
+ });
299
+ raw = (res.stdout || "").replace(/\r/g, "");
300
+ if (!raw.trim()) {
301
+ emit(((res.stderr || "").trim() || "[OMC HUD] no output") + "\n");
302
+ }
303
+ }
304
+
305
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
306
+
307
+ // Terminal columns of a string: ANSI is free, CJK and emoji take two columns,
308
+ // variation selectors and ZWJ take none.
309
+ const WRAP_MARGIN = 4;
310
+ // BMP emoji drawn two columns wide (East Asian Width W), e.g. ⚡ in OMC's call counts.
311
+ const WIDE_BMP_EMOJI = [
312
+ [0x231a, 0x231b], [0x23e9, 0x23ec], [0x23f0, 0x23f0], [0x23f3, 0x23f3],
313
+ [0x25fd, 0x25fe], [0x2614, 0x2615], [0x2648, 0x2653], [0x267f, 0x267f],
314
+ [0x2693, 0x2693], [0x26a1, 0x26a1], [0x26aa, 0x26ab], [0x26bd, 0x26be],
315
+ [0x26c4, 0x26c5], [0x26ce, 0x26ce], [0x26d4, 0x26d4], [0x26ea, 0x26ea],
316
+ [0x26f2, 0x26f3], [0x26f5, 0x26f5], [0x26fa, 0x26fa], [0x26fd, 0x26fd],
317
+ [0x2705, 0x2705], [0x270a, 0x270b], [0x2728, 0x2728], [0x274c, 0x274c],
318
+ [0x274e, 0x274e], [0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797],
319
+ [0x27b0, 0x27b0], [0x27bf, 0x27bf], [0x2b1b, 0x2b1c], [0x2b50, 0x2b50],
320
+ [0x2b55, 0x2b55],
321
+ ];
322
+ // Every two-column range: Hangul Jamo, the emoji above, CJK, Hangul syllables, CJK compatibility and
323
+ // forms, fullwidth forms, the emoji planes and the supplementary ideographs.
324
+ const WIDE_RANGES = [
325
+ [0x1100, 0x115f], ...WIDE_BMP_EMOJI, [0x2e80, 0xa4cf], [0xac00, 0xd7a3], [0xf900, 0xfaff], [0xfe30, 0xfe4f],
326
+ [0xff00, 0xff60], [0xffe0, 0xffe6], [0x1f300, 0x1faff], [0x20000, 0x10ffff],
327
+ ];
328
+ const isWide = (c) => WIDE_RANGES.some(([lo, hi]) => c >= lo && c <= hi);
329
+ const visibleWidth = (s) => {
330
+ let w = 0;
331
+ for (const ch of stripAnsi(s)) {
332
+ const c = ch.codePointAt(0);
333
+ if (c === 0x200d || (c >= 0xfe00 && c <= 0xfe0f)) continue;
334
+ w += isWide(c) ? 2 : 1;
335
+ }
336
+ return w;
337
+ };
338
+
339
+ // Color helpers — we strip OMC's ANSI to parse, then re-apply our own so the
340
+ // statusline is highlighted. Threshold scale (usage/context %): green<70,
341
+ // yellow<85, pink>=85 (pink replaces red on request). Pipes and path are
342
+ // dimmed; model name cyan, effort yellow.
343
+ const A = (c, s) => `\x1b[${c}m${s}\x1b[0m`;
344
+ const SEP = "\x1b[2m|\x1b[0m";
345
+ const PINK = "38;5;213";
346
+ const pc = (n) => (n >= 85 ? PINK : n >= 70 ? "33" : "32");
347
+ const colorRate = (seg) => {
348
+ const m = seg.match(/^(\w+):(\d+)%\/(.+)$/);
349
+ return m ? `${m[1]}:${A(pc(+m[2]), m[2] + "%")}\x1b[2m/\x1b[0m${A("36", m[3])}` : seg;
350
+ };
351
+ const colorCtx = (seg) => {
352
+ const m = seg.match(/^ctx:(\d+)%$/);
353
+ return m ? `ctx:${A(pc(+m[1]), m[1] + "%")}` : seg;
354
+ };
355
+ const colorModel = (seg) => {
356
+ const m = seg.match(/^(.+?)\/(\w+)$/);
357
+ return m ? `${A("36", m[1])}\x1b[2m/\x1b[0m${A("33", m[2])}` : A("36", seg);
358
+ };
359
+ const colorSe = (seg) => {
360
+ const m = seg.match(/^se:(.+)$/);
361
+ return m ? `se:${A("33", m[1])}` : seg;
362
+ };
363
+ // Per-model usage badge ("fable:0%") rides along in the rate group and has no
364
+ // dedicated field. Shorten the model name to two letters so it matches the
365
+ // model token's own convention (Opus 4.8 -> Op4.8): fable:0% -> Fa:0%.
366
+ // OMC appends a stale marker and/or a reset time to scoped weekly buckets
367
+ // (renderRateLimits in dist/hud/elements/limits.js), so the pattern must not
368
+ // anchor at "%": keep that tail intact -> fable:0%(3d3h) -> Fa:0%(3d3h).
369
+ const shortModelPct = (seg) => {
370
+ const m = seg.match(/^([A-Za-z][A-Za-z0-9.-]*):(\d+%\*?(?:\([^)]*\))?)$/);
371
+ return m ? `${m[1][0].toUpperCase()}${m[1].slice(1, 2).toLowerCase()}:${m[2]}` : seg;
372
+ };
373
+
374
+ try {
375
+ const lines = raw.split("\n").map(stripAnsi).filter((l) => l.length);
376
+ const mainIdx = lines.findIndex((l) => l.includes("OMC#"));
377
+ if (mainIdx === -1) throw new Error("no OMC line");
378
+
379
+ const main = lines[mainIdx];
380
+ const pathLine = lines.slice(0, mainIdx).join(" ").trim(); // cwd (+git) group above
381
+ // Detail lines (agent tree, warnings) are kept as-is, except OMC's update
382
+ // hints ("[!] omc 5.7.5 - paste: ! claude plugin ...", "[!] claude 2.1.302 -
383
+ // paste: ! claude update"): the OMC version is not wanted here, and the Claude
384
+ // Code update already shows as claude<cur>→<new> on the main line.
385
+ const UPDATE_HINT = /^\[!\] (?:omc|claude) \S+ - paste: /;
386
+ const below = lines.slice(mainIdx + 1).filter((l) => !UPDATE_HINT.test(l.trim()));
387
+
388
+ // Collapse " | " -> "|", then split into segments.
389
+ const rawSegs = main
390
+ .replace(/\s*\|\s*/g, "|")
391
+ .split("|")
392
+ .map((s) => s.trim())
393
+ .filter(Boolean);
394
+
395
+ // The 5h/wk/sn rate windows live in one space-separated segment; expand them.
396
+ // The per-model usage badge ("fable:0%") arrives inside that same segment, so
397
+ // remember which pieces came from there. Only those get abbreviated later —
398
+ // unrelated badges that also land in `extra` must be left alone.
399
+ const segs = [];
400
+ const rateGroup = new Set();
401
+ // OMC's Claude Code update notice ("[Claude#2.1.300] -> 2.1.302 claude update",
402
+ // render.js claudeLabel) is shortened to "claude2.1.300→2.1.302" and shown last.
403
+ // It is taken out of segs, so a claude-update-last block (which looks for the
404
+ // long form in segs) finds nothing to move.
405
+ const UPDATE_NOTICE = /^\[Claude(?:#([^\]]*))?\]\s*->\s*(\S+)\s+claude\s+update\b/;
406
+ let update = "";
407
+ for (const s of rawSegs) {
408
+ const um = s.match(UPDATE_NOTICE);
409
+ if (um) {
410
+ update = `claude${um[1] || ""}→${um[2]}`;
411
+ } else if (/\b(5h|wk|sn):/.test(s) && /\s/.test(s)) {
412
+ for (const p of s.split(/\s+/)) {
413
+ if (!p) continue;
414
+ segs.push(p);
415
+ if (!/^(5h|wk|sn):/.test(p)) rateGroup.add(p);
416
+ }
417
+ } else {
418
+ segs.push(s);
419
+ }
420
+ }
421
+
422
+ // Without effort.level on stdin, take the level from OMC's effort badge.
423
+ if (!effort) {
424
+ const badge = segs.find((s) => /^effort:\S/.test(s));
425
+ if (badge) effort = badge.slice("effort:".length).trim();
426
+ }
427
+
428
+ const f = { label: "", model: "", r5: "", rwk: "", rsn: "", think: "", ctx: "", se: "", counts: "" };
429
+ let ctxShown = ""; // the ctx segment shown after se; f.ctx stays empty (see the ctx line below)
430
+ const extra = [];
431
+ const modelRates = []; // per-model usage badges, shown right after the rate windows
432
+ const slash = (s) => s.replace(/%\(([^)]+)\)/, "%/$1"); // 6%(3h56m) -> 6%/3h56m
433
+
434
+ for (const s of segs) {
435
+ if (/^\[?OMC#/.test(s)) {
436
+ const m = s.match(/OMC#([0-9][0-9.]*)/);
437
+ f.label = m ? `OMC#${m[1]}` : s; // drop brackets + trailing "L"
438
+ } else if (/^Model:/.test(s) || /^(Opus|Sonnet|Haiku)\b/.test(s)) {
439
+ const m = s.match(/(?:Model:\s*)?([A-Za-z]+)\s*([0-9][0-9.]*)/);
440
+ // Opus 4.8 -> Op4.8/high; an unknown id (Model: claude-x-5) keeps its name.
441
+ const name = m ? `${m[1].slice(0, 2)}${m[2]}` : s.replace(/^Model:\s*/, "");
442
+ f.model = `${name}${effort ? "/" + effort : ""}`;
443
+ } else if (/^5h:/.test(s)) {
444
+ f.r5 = slash(s);
445
+ } else if (/^wk:/.test(s)) {
446
+ f.rwk = slash(s);
447
+ } else if (/^sn:/.test(s)) {
448
+ f.rsn = slash(s);
449
+ } else if (/^think/i.test(s)) {
450
+ f.think = "think";
451
+ } else if (/^effort:/.test(s)) {
452
+ // OMC 5.x effort badge; the level already rides on the model (Op5.5/high).
453
+ } else if (/^ctx:/.test(s)) {
454
+ // "ctx:82% COMPRESS?" -> "ctx:82%": the color already says how full it is.
455
+ const m = s.match(/^ctx:(\d+)%/);
456
+ ctxShown = m ? `ctx:${m[1]}%` : s;
457
+ } else if (/^session:/.test(s)) {
458
+ const m = s.match(/session:(\d+)m/);
459
+ f.se = m ? `se:${(parseInt(m[1], 10) / 60).toFixed(1)}h` : s.replace(/^session:/, "se:");
460
+ } else if (/🔧|🤖|⚡/.test(s) || /^[TAS]:\d/.test(s)) {
461
+ f.counts = s;
462
+ } else {
463
+ if (rateGroup.has(s)) modelRates.push(slash(shortModelPct(s))); // fable:0%(1h31m) -> Fa:0%/1h31m
464
+ else extra.push(s); // dynamic badges: todos, ralph, autopilot, bg, skill, etc.
465
+ }
466
+ }
467
+
468
+ const colored = [];
469
+ if (f.model) colored.push(colorModel(f.model));
470
+ if (f.r5) colored.push(colorRate(f.r5));
471
+ if (f.rwk) colored.push(colorRate(f.rwk));
472
+ if (f.rsn) colored.push(colorRate(f.rsn));
473
+ for (const r of modelRates) colored.push(colorRate(r));
474
+ if (f.think) colored.push(A("36", f.think));
475
+ // Keep the next line: f.ctx is always empty now (ctx is shown after se), but
476
+ // `payload-mon on` inserts its block right after this exact line, ahead of PL.
477
+ if (f.ctx) colored.push(colorCtx(f.ctx));
478
+ // PL: payload size always shown before se, as "PL:0.3M" (MB, one decimal),
479
+ // colored by its share of the 32MB limit. Replaces the payload-mon segment,
480
+ // which only appears from 8MB on. Reuses payload-mon's module copy and cache;
481
+ // without it (payload-mon off) or without a transcript, PL is left out.
482
+ try {
483
+ const transcript = JSON.parse(input)?.transcript_path;
484
+ if (transcript) {
485
+ const { estimatePayloadBytes, REQUEST_LIMIT_BYTES } = await import(pathToFileURL(join(__dirname, "payload-mon", "payload-size.mjs")).href);
486
+ const bytes = estimatePayloadBytes(transcript);
487
+ if (Number.isFinite(bytes)) {
488
+ for (let i = colored.length - 1; i >= 0; i--) if (/^payload:/.test(stripAnsi(colored[i]))) colored.splice(i, 1);
489
+ const pct = Math.floor((bytes / REQUEST_LIMIT_BYTES) * 100);
490
+ colored.push(`PL:${A(pc(pct), (bytes / (1024 * 1024)).toFixed(1) + "M")}`);
491
+ }
492
+ }
493
+ } catch {
494
+ /* no stdin JSON, unreadable transcript or module missing */
495
+ }
496
+ if (f.se) colored.push(colorSe(f.se));
497
+ if (ctxShown) colored.push(colorCtx(ctxShown));
498
+ // OMC prints "🔧14 🤖1 ⚡3" ("T:14 A:1 S:3" in ASCII) and leaves out a zero count.
499
+ // Show agents, tools, skills, one segment each; anything else keeps its place after them.
500
+ if (f.counts) {
501
+ const rank = (p) => (/^(?:🤖|A:)/u.test(p) ? 0 : /^(?:🔧|T:)/u.test(p) ? 1 : /^(?:⚡|S:)/u.test(p) ? 2 : 3);
502
+ colored.push(...f.counts.trim().split(/\s+/).sort((a, b) => rank(a) - rank(b)));
503
+ }
504
+ for (const e of extra) colored.push(A("2", e));
505
+ // OMC's cwd group is "cwd | branch:x | ...": keep the cwd parts here with "|",
506
+ // and move the branch (without its "branch:" label) right before OMC#.
507
+ let branch = "";
508
+ const pathParts = pathLine
509
+ .split(/\s*\|\s*/)
510
+ .map((p) => p.trim())
511
+ .filter((p) => {
512
+ const m = p.match(/^branch:\s*(.+)$/);
513
+ if (m) branch = m[1];
514
+ return p && !m;
515
+ });
516
+ for (const p of pathParts) colored.push(A("36", p));
517
+ const acct = loginAccount();
518
+ if (acct) colored.push(A("92", acct));
519
+ if (branch) colored.push(A("36", branch));
520
+ // The OMC#X.Y.Z label is parsed (it marks OMC's main line) but not shown.
521
+ if (update) colored.push(A("2", update));
522
+
523
+ let result = colored.join(SEP);
524
+ // Wrap at segment boundaries instead of letting Claude Code truncate the tail.
525
+ // WRAP_MARGIN leaves room for the indent Claude Code puts before the line.
526
+ if (TERM_COLS > WRAP_MARGIN + 20) {
527
+ const max = TERM_COLS - WRAP_MARGIN;
528
+ const rows = [];
529
+ let row = "";
530
+ for (const seg of colored) {
531
+ if (row && visibleWidth(row) + 1 + visibleWidth(seg) > max) {
532
+ rows.push(row);
533
+ row = seg;
534
+ } else {
535
+ row = row ? row + SEP + seg : seg;
536
+ }
537
+ }
538
+ if (row) rows.push(row);
539
+ result = rows.join("\n");
540
+ }
541
+ if (below.length) result += "\n" + below.join("\n");
542
+ emit(result + "\n");
543
+ } catch {
544
+ emit(raw.endsWith("\n") ? raw : raw + "\n"); // safe passthrough
545
+ }
@@ -28,7 +28,7 @@
28
28
  import { spawnSync } from "node:child_process";
29
29
  import { accessSync, constants, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
30
30
  import { homedir, tmpdir } from "node:os";
31
- import { join } from "node:path";
31
+ import { join, resolve } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
 
34
34
  const OPEN = "// >>> claude-update-last >>>";
@@ -42,7 +42,16 @@ const JOIN_LINE = /^ {2}let result = colored\.join\(SEP\);\r?$/gm;
42
42
  // What the block reads at that line, all in the wrapper's own scope there.
43
43
  const NAMES = [/\bconst segs\b/, /\bconst colored\b/, /\bconst stripAnsi\b/, /\bconst A = /];
44
44
 
45
- const CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
45
+ // The same rules as ../assets/apply-hud.mjs (OMC's getClaudeConfigDir): trim, expand a leading "~",
46
+ // make it absolute, drop a trailing separator, so this finds the wrapper the installer wrote.
47
+ function configDir() {
48
+ const v = (process.env.CLAUDE_CONFIG_DIR || "").trim();
49
+ if (!v) return join(homedir(), ".claude");
50
+ if (v === "~") return homedir();
51
+ if (v.startsWith("~/") || v.startsWith("~\\")) return join(homedir(), v.slice(2));
52
+ return resolve(v);
53
+ }
54
+ const CONFIG_DIR = configDir().replace(/[\\/]+$/, "");
46
55
 
47
56
  const DEFAULTS = {
48
57
  hudFile: process.env.CLAUDE_UPDATE_LAST_HUD_FILE || join(CONFIG_DIR, "hud", "omc-hud-custom.mjs"),