@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +6 -3
- package/codex/manifest.json +1 -1
- package/commands/setup.md +1 -1
- package/package.json +1 -1
- package/skills/payload-mon/SKILL.md +6 -4
- package/skills/payload-mon/scripts/payload-mon.mjs +12 -2
- package/skills/setup-omc-hud/SKILL.md +113 -52
- package/skills/setup-omc-hud/assets/LICENSE-omc_hud +21 -0
- package/skills/setup-omc-hud/assets/apply-hud.mjs +234 -0
- package/skills/setup-omc-hud/assets/hud-config.json +53 -0
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +545 -0
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +11 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "banker",
|
|
3
|
-
"version": "0.
|
|
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 상태표시줄
|
|
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`
|
|
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` 가
|
|
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 는 그 회사들과 관계가 없습니다.
|
package/codex/manifest.json
CHANGED
|
@@ -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
|
|
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 상태표시줄(
|
|
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.
|
|
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
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
+
# setup-omc-hud: omc_hud 상태표시줄 설치 (오프라인)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
이 스킬 폴더의 `assets/` 에 든 파일로 상태표시줄 래퍼 v4 를 설치한다. 답변은 한글.
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
|
|
69
|
+
PowerShell:
|
|
24
70
|
```powershell
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
`
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
-
|
|
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
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
- `
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
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
|
-
|
|
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"),
|