pinglet-cli 0.6.2 → 0.7.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/README.ja.md CHANGED
@@ -58,6 +58,46 @@ pinglet post "メッセージ"
58
58
 
59
59
  statusline の言語はシステム言語に応じて自動選択されます(韓国語/日本語、それ以外は英語)。言語で判定できない場合はタイムゾーン(ソウル/東京)で推定します。
60
60
 
61
+ ## HUD — セッション情報を statusline に
62
+
63
+ 「一緒にコーディング中」の行の下に、[claude-hud](https://github.com/jarrodwatts/claude-hud) と同じ方式の HUD が表示されます。
64
+ モデルと effort レベル、プロジェクト・git ブランチ(変更行数・未プッシュのコミット)、`/advisor` モデル、セッション時間・コスト・出力速度、
65
+ コンテキスト使用率バー、5時間/7日/モデル別週間の使用量上限、prompt cache の期限時刻、端末の RAM 使用率、
66
+ 実行中のツール・呼び出したスキル・サブエージェント・todo の進捗、変更ファイル一覧、セッション累計トークン、圧縮回数です。
67
+
68
+ ```
69
+ 🟢 いま41個のターミナルと一緒にコーディング中
70
+ [Opus 5 ◑ high] │ my-project git:(main* ↑2 [+337 -29]) │ アドバイザー: Opus 4.7 │ ⏱ 56m │ コスト $1.23 │ 出力: 42.1 tok/s
71
+ コンテキスト ████░░░░░░ 45% │ 使用量 ███░░░░░░░ 31% (リセットまで 1h 7m) | 週間 █████████░ 85% (リセットまで 2d 7h) │ キャッシュ ⏱ 期限 22:12 · ヒット 98%
72
+ RAM █████░░░░░ 23 GB / 48 GB (48%)
73
+ ◐ Edit: .../file.ts | ✓ Bash ×12 | ✓ Read ×3
74
+ ✓ スキル (2): pinglet, code-review
75
+ ◐ Explore [haiku-4.5]: 呼び出し元を探す (12s)
76
+ ▸ バグ修正 (2/5)
77
+ ~statusline.ts(+39 -11) ~cli.ts(+15 -5) +hud.ts(+120) ?3
78
+ トークン 13.7M (入力: 2k, 出力: 64k, キャッシュ: 13.7M)
79
+ 圧縮: 1
80
+ ```
81
+
82
+ キャッシュ行のヒット率は直近リクエストの入力のうちキャッシュから読まれた割合(cache_read / 入力合計)で、キャッシュ期限切れ後に書き直したターンで大きく下がります。prompt cache はカウントダウンではなく期限時刻で表示します。statusline は Claude が動作中にしか再描画されず、
83
+ ターン間では残り時間が止まったまま見えるためです。RAM は Claude プロセスではなく端末全体の値です。
84
+ 端末幅を超える行は区切り(`│`, `|`)で次の行に折り返します。
85
+ コンテキスト・使用量・キャッシュは既定で1行にまとめられ(`display.mergeGroups`)、幅が足りなければ自動的に分かれます。
86
+
87
+ 何を・どの順で・何行に表示するかは `pinglet hud` で変更します(Claude Code 内では `/pinglet-hud`):
88
+
89
+ ```bash
90
+ pinglet hud # 現在の設定 + プレビュー
91
+ pinglet hud --preset minimal # full(すべて) / essential(モデル・プロジェクト・コンテキスト・使用量・キャッシュ・アクティビティ) / minimal(モデル+コンテキスト)
92
+ pinglet hud --layout compact # 1行にまとめる(expanded は要素ごとに1行)
93
+ pinglet hud --hide usage,todos # --show で再表示(speed, effort, session-tokens, compactions, git-files なども)
94
+ pinglet hud --order context,project # 行の順序(省いた要素は非表示)
95
+ pinglet hud --first-line project,model # 1行目のセグメント順
96
+ pinglet hud --off # HUD のみオフ(「一緒にコーディング中」の行は残る)
97
+ ```
98
+
99
+ 色・しきい値・mergeGroups などの上級キーは `~/.pinglet/config.json` の `"hud"` セクションを直接編集します(キー名は claude-hud と同じです)。
100
+
61
101
  ## 自動アップデート
62
102
 
63
103
  新しいバージョンが出ると、バックグラウンドで1日1回チェックして自動的に
@@ -73,6 +113,7 @@ statusline の言語はシステム言語に応じて自動選択されます(
73
113
  | `pinglet logout` | ログアウト(連携設定・キャッシュは維持) |
74
114
  | `pinglet post "メッセージ"` | Ping を送る |
75
115
  | `pinglet ping` | いま表示されるメッセージをプレビュー |
116
+ | `pinglet hud [オプション]` | statusline HUD の設定(プリセット・レイアウト・表示要素・順序) |
76
117
  | `pinglet doctor` | インストール・接続状態を診断 |
77
118
  | `pinglet uninstall` | 設定復元、サーバー上のインストール失効とログアウト |
78
119
 
@@ -80,8 +121,11 @@ statusline の言語はシステム言語に応じて自動選択されます(
80
121
 
81
122
  - **トークン使用量 0** — メッセージは Claude Code の UI 領域(statusline/spinner)にのみ
82
123
  表示され、モデルのコンテキストには一切入りません。API コストや応答品質に影響はありません。
83
- - **コードを読みません** — prompt、応答、コード、環境変数にはアクセスしません。
84
- 収集するのはインストール ID、OS の種類、クライアントのバージョン、メッセージ表示イベントだけです。
124
+ - **コードを送信しません** — サーバーに送るのはインストール ID、OS の種類、クライアントのバージョン、
125
+ メッセージ表示イベントだけです。HUD は Claude Code が statusline に渡すセッション指標と、
126
+ セッション transcript のツール名・対象・todo タイトルを**この端末内でのみ**読んで描画します。
127
+ prompt・応答本文は保存せず、それらが端末の外に出ることはありません。
128
+ 活動行が不要なら `pinglet hud --hide tools,agents,todos` で transcript を一切読まなくなります。
85
129
  - **ターミナルが遅くなりません** — 表示はローカルキャッシュを読むだけで、ネットワークは
86
130
  バックグラウンドでのみ使用します。オフラインでも動作します。
87
131
  - **すべてのメッセージは審査を通ります** — URL・個人情報・制御文字・不適切な表現は
package/README.ko.md CHANGED
@@ -58,6 +58,46 @@ pinglet post "메시지"
58
58
 
59
59
  statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어/일본어, 그 외는 영어). 시스템 언어로 판정이 안 되면 타임존(서울/도쿄)으로 추정합니다.
60
60
 
61
+ ## HUD — 세션 정보를 statusline에
62
+
63
+ "함께 코딩 중" 줄 아래에 [claude-hud](https://github.com/jarrodwatts/claude-hud)와 같은 방식의 HUD가 표시됩니다.
64
+ 모델과 effort 레벨, 프로젝트·git 브랜치(변경 줄 수·푸시 안 한 커밋), `/advisor` 자문 모델, 세션 시간·비용·출력 속도,
65
+ 컨텍스트 사용률 바, 5시간/7일/모델별 주간 사용량 한도, prompt cache 만료 시각, 기기 RAM 사용률,
66
+ 지금 도는 도구·호출한 스킬·서브 에이전트·todo 진행률, 변경 파일 목록, 세션 누적 토큰, 압축 횟수입니다.
67
+
68
+ ```
69
+ 🟢 지금 41개 터미널과 함께 코딩 중
70
+ [Opus 5 ◑ high] │ my-project git:(main* ↑2 [+337 -29]) │ 자문 모델: Opus 4.7 │ ⏱ 56m │ 비용 $1.23 │ 출력: 42.1 tok/s
71
+ 컨텍스트 ████░░░░░░ 45% │ 사용량 ███░░░░░░░ 31% (리셋까지 1h 7m) | 주간 █████████░ 85% (리셋까지 2d 7h) │ 캐시 ⏱ 만료 22:12 · 히트 98%
72
+ RAM █████░░░░░ 23 GB / 48 GB (48%)
73
+ ◐ Edit: .../file.ts | ✓ Bash ×12 | ✓ Read ×3
74
+ ✓ 스킬 (2): pinglet, code-review
75
+ ◐ Explore [haiku-4.5]: 호출부 찾기 (12s)
76
+ ▸ 버그 수정 (2/5)
77
+ ~statusline.ts(+39 -11) ~cli.ts(+15 -5) +hud.ts(+120) ?3
78
+ 토큰 13.7M (입력: 2k, 출력: 64k, 캐시: 13.7M)
79
+ 압축: 1
80
+ ```
81
+
82
+ 캐시 줄의 히트율은 마지막 요청에서 캐시로 읽은 토큰의 비율(cache_read / 전체 입력)이라, 캐시가 만료돼 새로 쓴 턴에서 뚝 떨어집니다. prompt cache는 카운트다운이 아니라 만료 시각으로 보여줍니다. statusline은 Claude가 활동할 때만 다시 그려져서
83
+ 턴 사이에는 남은 시간이 멈춘 채 보이기 때문입니다. RAM은 Claude 프로세스가 아니라 기기 전체 수치입니다.
84
+ 터미널 폭을 넘는 줄은 구분자(`│`, `|`)에서 다음 줄로 감깁니다.
85
+ 컨텍스트·사용량·캐시는 기본적으로 한 줄로 묶이며(`display.mergeGroups`), 폭이 모자라면 자동으로 나뉩니다.
86
+
87
+ 무엇을, 어떤 순서로, 몇 줄에 보여줄지는 `pinglet hud`로 바꿉니다 (Claude Code 안에서는 `/pinglet-hud`):
88
+
89
+ ```bash
90
+ pinglet hud # 현재 설정 + 미리보기
91
+ pinglet hud --preset minimal # full(전부) / essential(모델·프로젝트·컨텍스트·사용량·캐시·활동) / minimal(모델+컨텍스트)
92
+ pinglet hud --layout compact # 한 줄로 (expanded는 요소마다 한 줄)
93
+ pinglet hud --hide usage,todos # --show 로 다시 켜기 (speed, effort, session-tokens, compactions, git-files 등)
94
+ pinglet hud --order context,project # 줄 순서 (뺀 요소는 숨김)
95
+ pinglet hud --first-line project,model # 첫 줄 안의 세그먼트 순서
96
+ pinglet hud --off # HUD만 끄기 ("함께 코딩 중" 줄은 유지)
97
+ ```
98
+
99
+ 색상·임계값·mergeGroups 같은 고급 키는 `~/.pinglet/config.json`의 `"hud"` 섹션을 직접 편집합니다 (키 이름은 claude-hud와 같습니다).
100
+
61
101
  ## 자동 업데이트
62
102
 
63
103
  새 버전이 나오면 백그라운드에서 하루 1회 확인해 자동으로 업데이트합니다
@@ -73,6 +113,7 @@ statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어
73
113
  | `pinglet logout` | 로그인 해제 (설치·캐시는 유지) |
74
114
  | `pinglet post "메시지"` | Ping 보내기 |
75
115
  | `pinglet ping` | 지금 표시될 메시지 미리보기 |
116
+ | `pinglet hud [옵션]` | statusline HUD 설정 (프리셋·레이아웃·표시 요소·순서) |
76
117
  | `pinglet doctor` | 설치·연결 상태 진단 |
77
118
  | `pinglet uninstall` | 기존 설정 복원, 서버 설치 폐기 및 로그아웃 |
78
119
 
@@ -81,8 +122,11 @@ statusline 언어는 시스템 언어에 따라 자동 선택됩니다(한국어
81
122
  - **토큰 사용량 0** — 메시지는 Claude Code의 UI 영역(statusline/spinner)에만
82
123
  표시되고 모델 컨텍스트에는 전혀 들어가지 않습니다. API 비용과 응답 품질에
83
124
  영향이 없습니다.
84
- - **코드를 읽지 않습니다** — prompt, 응답, 코드, 환경변수에 접근하지 않습니다.
85
- 수집하는 것은 설치 ID, OS 종류, 클라이언트 버전, 메시지 노출 이벤트뿐입니다.
125
+ - **코드를 보내지 않습니다** — 서버로 가는 것은 설치 ID, OS 종류, 클라이언트 버전,
126
+ 메시지 노출 이벤트뿐입니다. HUD는 Claude Code가 statusline에 넘기는 세션 지표와
127
+ 세션 transcript의 도구 호출 이름·대상·todo 제목만 **이 기기 안에서** 읽어 그리며,
128
+ prompt·응답 본문은 저장하지 않고 어떤 것도 서버로 보내지 않습니다.
129
+ HUD의 활동 줄이 필요 없으면 `pinglet hud --hide tools,agents,todos`로 끄면 transcript를 읽지 않습니다.
86
130
  - **터미널이 느려지지 않습니다** — 표시는 로컬 캐시만 읽어서 그리며, 네트워크는
87
131
  백그라운드에서만 사용합니다. 오프라인에서도 동작합니다.
88
132
  - **모든 메시지는 검수를 거칩니다** — URL·개인정보·제어문자·부적절한 표현은
package/README.md CHANGED
@@ -58,6 +58,52 @@ The statusline shows how many terminals are coding alongside you right now:
58
58
 
59
59
  The statusline picks its language from your system language (Korean/Japanese, English otherwise), falling back to your timezone (Seoul/Tokyo) when the language doesn't decide it.
60
60
 
61
+ ## HUD — session metrics in the statusline
62
+
63
+ Below the "coding along" line Pinglet renders a HUD that works the same way as
64
+ [claude-hud](https://github.com/jarrodwatts/claude-hud): model and effort level,
65
+ project and git branch (line diff, unpushed commits), the `/advisor` model, session
66
+ duration, cost and output speed, a context-usage bar, 5-hour / 7-day / per-model
67
+ weekly usage limits, the prompt-cache expiry time, machine RAM usage, the tools,
68
+ skills, subagents and todo progress of the current session, changed files, session
69
+ token totals and compaction count.
70
+
71
+ ```
72
+ 🟢 coding along with 41 terminals right now
73
+ [Opus 5 ◑ high] │ my-project git:(main* ↑2 [+337 -29]) │ Advisor: Opus 4.7 │ ⏱ 56m │ Cost $1.23 │ out: 42.1 tok/s
74
+ Context ████░░░░░░ 45% │ Usage ███░░░░░░░ 31% (resets in 1h 7m) | Weekly █████████░ 85% (resets in 2d 7h) │ Cache ⏱ until 10:12 PM · hit 98%
75
+ Approx RAM █████░░░░░ 23 GB / 48 GB (48%)
76
+ ◐ Edit: .../file.ts | ✓ Bash ×12 | ✓ Read ×3
77
+ ✓ Skills (2): pinglet, code-review
78
+ ◐ Explore [haiku-4.5]: Find callers (12s)
79
+ ▸ Fix the bug (2/5)
80
+ ~statusline.ts(+39 -11) ~cli.ts(+15 -5) +hud.ts(+120) ?3
81
+ Tokens 13.7M (in: 2k, out: 64k, cache: 13.7M)
82
+ Compactions: 1
83
+ ```
84
+
85
+ The hit rate on the Cache line is the share of the last request's input that was served from cache (cache_read / total input), so it drops sharply on the turn after the cache expires. The prompt cache shows an expiry time rather than a countdown: the statusline only
86
+ repaints while Claude is active, so a countdown would freeze between turns. RAM is
87
+ the whole machine, not the Claude process. Lines wider than the terminal wrap at
88
+ their separators (`│`, `|`). Context, usage and the cache line share one row by default
89
+ (`display.mergeGroups`) and split apart when the terminal is too narrow.
90
+
91
+ Choose what to show, in which order and on how many lines with `pinglet hud`
92
+ (`/pinglet-hud` inside Claude Code):
93
+
94
+ ```bash
95
+ pinglet hud # current settings + preview
96
+ pinglet hud --preset minimal # full (everything) / essential (model, project, context, usage, cache, activity) / minimal (model + context)
97
+ pinglet hud --layout compact # everything on one line (expanded = one line per element)
98
+ pinglet hud --hide usage,todos # --show to turn them back on (also speed, effort, session-tokens, compactions, git-files, …)
99
+ pinglet hud --order context,project # line order (omitted elements are hidden)
100
+ pinglet hud --first-line project,model # segment order inside the first line
101
+ pinglet hud --off # HUD only; the "coding along" line stays
102
+ ```
103
+
104
+ Advanced keys (colors, thresholds, mergeGroups) live in the `"hud"` section of
105
+ `~/.pinglet/config.json` and use the same names as claude-hud.
106
+
61
107
  ## Auto-update
62
108
 
63
109
  Pinglet checks for a new version once a day in the background and updates
@@ -73,6 +119,7 @@ left untouched). To opt out, add `"autoUpdate": false` to `~/.pinglet/config.jso
73
119
  | `pinglet logout` | Sign out on this machine (keeps the integration and cache) |
74
120
  | `pinglet post "message"` | Send a Ping |
75
121
  | `pinglet ping` | Preview the message that would be shown now |
122
+ | `pinglet hud [options]` | Configure the statusline HUD (preset, layout, elements, order) |
76
123
  | `pinglet doctor` | Diagnose installation and connection status |
77
124
  | `pinglet uninstall` | Restore settings, revoke installations and sign out |
78
125
 
@@ -81,9 +128,13 @@ left untouched). To opt out, add `"autoUpdate": false` to `~/.pinglet/config.jso
81
128
  - **Zero token usage** — messages are shown only in Claude Code's UI area
82
129
  (statusline/spinner) and never enter the model context. No impact on API cost
83
130
  or response quality.
84
- - **Never reads your code** — no access to prompts, responses, code, or
85
- environment variables. The only data collected: an install ID, OS type,
86
- client version, and message-impression events.
131
+ - **Never sends your code** — the only data sent to the server is an install ID,
132
+ OS type, client version, and message-impression events. The HUD reads the
133
+ session metrics Claude Code hands to the statusline plus tool names/targets and
134
+ todo titles from the session transcript, **on this machine only**; prompt and
135
+ response bodies are never stored and nothing from them leaves your machine.
136
+ If you don't want the activity lines, `pinglet hud --hide tools,agents,todos`
137
+ stops the transcript from being read at all.
87
138
  - **Doesn't slow down your terminal** — rendering reads only a local cache;
88
139
  the network is used in the background only. Works offline too.
89
140
  - **Every message is moderated** — URLs, personal information, control
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.detectClaude = detectClaude;
37
+ exports.ensureSlashCommands = ensureSlashCommands;
37
38
  exports.armSpinnerMessage = armSpinnerMessage;
38
39
  exports.spinnerVerbsStatus = spinnerVerbsStatus;
39
40
  exports.isClaudeIntegrationInstalled = isClaudeIntegrationInstalled;
@@ -53,8 +54,8 @@ const SETTINGS_PATH = path.join(CLAUDE_DIR, "settings.json");
53
54
  const COMMANDS_DIR = path.join(CLAUDE_DIR, "commands");
54
55
  const COMMAND_MARKER = "generated by pinglet";
55
56
  /** 슬래시 명령이 호출하는 CLI 서브커맨드 — 권한 규칙과 command 파일 모두 이 목록 기준. */
56
- const SLASH_SUBCOMMANDS = ["post", "login", "logout"];
57
- const COMMAND_FILES = ["pinglet.md", "pinglet-login.md", "pinglet-logout.md"];
57
+ const SLASH_SUBCOMMANDS = ["post", "login", "logout", "hud"];
58
+ const COMMAND_FILES = ["pinglet.md", "pinglet-login.md", "pinglet-logout.md", "pinglet-hud.md"];
58
59
  /**
59
60
  * Claude Adapter.
60
61
  * Claude Code의 statusLine hook에 `pinglet statusline`을 연결한다.
@@ -142,6 +143,17 @@ allowed-tools: Bash(node:*)
142
143
 
143
144
  Result: !\`node "${cli}" logout\`
144
145
 
146
+ Output the result line above exactly as-is, as a single line, without adding or changing anything.
147
+ Do not add any explanation, guidance, or perform any other action.
148
+ `,
149
+ "pinglet-hud.md": `---
150
+ description: ${(0, i18n_1.t)("slash.hudDesc")}
151
+ allowed-tools: Bash(node:*)
152
+ ---
153
+ <!-- ${COMMAND_MARKER} -->
154
+
155
+ Result: !\`node "${cli}" hud --quiet $ARGUMENTS\`
156
+
145
157
  Output the result line above exactly as-is, as a single line, without adding or changing anything.
146
158
  Do not add any explanation, guidance, or perform any other action.
147
159
  `,
@@ -150,6 +162,17 @@ Do not add any explanation, guidance, or perform any other action.
150
162
  fs.writeFileSync(path.join(COMMANDS_DIR, name), body);
151
163
  }
152
164
  }
165
+ /**
166
+ * 누락된 slash command 파일만 채운다 — 자동 업데이트가 postinstall 없이 끝났을 때
167
+ * 새 명령(`/pinglet-hud` 등)이 생기도록 refresh가 호출한다. 있는 파일은 건드리지 않는다.
168
+ */
169
+ function ensureSlashCommands(config) {
170
+ if (!config.adapters.claude)
171
+ return;
172
+ const missing = COMMAND_FILES.some((name) => !fs.existsSync(path.join(COMMANDS_DIR, name)));
173
+ if (missing)
174
+ installSlashCommand();
175
+ }
153
176
  /** Pinglet이 생성한 command 파일만 삭제한다 (마커로 확인). */
154
177
  function uninstallSlashCommand() {
155
178
  let removed = false;
package/dist/cli.js CHANGED
@@ -12,6 +12,7 @@ const flush_1 = require("./commands/flush");
12
12
  const refresh_1 = require("./commands/refresh");
13
13
  const ping_1 = require("./commands/ping");
14
14
  const post_1 = require("./commands/post");
15
+ const hud_1 = require("./commands/hud");
15
16
  const i18n_1 = require("./i18n");
16
17
  const lock_1 = require("./lock");
17
18
  const notifications_1 = require("./notifications");
@@ -41,9 +42,17 @@ async function main() {
41
42
  case "ping":
42
43
  (0, ping_1.runPing)();
43
44
  break;
44
- case "statusline":
45
- (0, statusline_1.runStatusline)();
45
+ case "hud": {
46
+ const quiet = rest.includes("--quiet");
47
+ await (0, hud_1.runHud)(rest.filter((arg) => arg !== "--quiet"), { quiet });
46
48
  break;
49
+ }
50
+ case "statusline": {
51
+ const tick = (0, statusline_1.runStatusline)();
52
+ if (tick)
53
+ await (0, statusline_1.writeStatusline)(tick);
54
+ break;
55
+ }
47
56
  case "flush":
48
57
  await (0, flush_1.runFlush)();
49
58
  break;
@@ -79,11 +88,13 @@ async function dispatch() {
79
88
  return;
80
89
  }
81
90
  if (command === "notify-drain" || command === "statusline") {
82
- await (0, lock_1.withPingletLock)(() => {
91
+ // 잠금은 상태 파일 읽기/쓰기 구간만 보호한다. transcript·git을 읽는 HUD 렌더는 잠금 밖에서.
92
+ const tick = await (0, lock_1.withPingletLock)(() => {
83
93
  (0, notifications_1.drainNotifications)();
84
- if (command === "statusline")
85
- (0, statusline_1.runStatusline)();
94
+ return command === "statusline" ? (0, statusline_1.runStatusline)() : undefined;
86
95
  }, { skipIfBusy: command === "statusline" });
96
+ if (tick)
97
+ await (0, statusline_1.writeStatusline)(tick);
87
98
  return;
88
99
  }
89
100
  if (["--version", "-v", "help", "--help"].includes(command)) {
@@ -8,6 +8,7 @@ const queue_1 = require("../queue");
8
8
  const claude_1 = require("../adapters/claude");
9
9
  const codex_1 = require("../adapters/codex");
10
10
  const i18n_1 = require("../i18n");
11
+ const hud_1 = require("../hud");
11
12
  const ok = (label) => console.log(`✓ ${label}`);
12
13
  const warn = (label) => console.log(`○ ${label}`);
13
14
  const bad = (label) => console.log(`✗ ${label}`);
@@ -31,6 +32,8 @@ async function runDoctor() {
31
32
  else {
32
33
  warn((0, i18n_1.t)("doctor.spinnerMissing"));
33
34
  }
35
+ const hud = (0, hud_1.loadHudConfig)(config);
36
+ ok((0, i18n_1.t)("doctor.hud", { enabled: hud.enabled ? "on" : "off", layout: hud.lineLayout }));
34
37
  }
35
38
  else
36
39
  warn((0, i18n_1.t)("doctor.claudeNotInstalled"));
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeHudArgs = normalizeHudArgs;
4
+ exports.parseHudArgs = parseHudArgs;
5
+ exports.applyHudArgs = applyHudArgs;
6
+ exports.runHud = runHud;
7
+ const config_1 = require("../config");
8
+ const config_2 = require("../hud/config");
9
+ const hud_1 = require("../hud");
10
+ const i18n_1 = require("../i18n");
11
+ /**
12
+ * `pinglet hud` — statusline HUD 설정.
13
+ * claude-hud의 configure 흐름(프리셋 → 끄기/켜기 → 레이아웃)을 플래그로 옮겼다.
14
+ * 인자 없이 실행하면 현재 설정과 미리보기를 보여준다. `/pinglet-hud`에서도 같은 인자를 받는다.
15
+ */
16
+ const PRESETS = ["full", "essential", "minimal"];
17
+ function splitList(value) {
18
+ return (value ?? "").split(/[,\s]+/).map((v) => v.trim()).filter(Boolean);
19
+ }
20
+ function onOff(value) {
21
+ if (value === "on" || value === "true" || value === "1")
22
+ return true;
23
+ if (value === "off" || value === "false" || value === "0")
24
+ return false;
25
+ return undefined;
26
+ }
27
+ /** 값을 하나 더 받는 플래그 — 그 뒤 토큰은 값이므로 플래그로 승격하지 않는다. */
28
+ const VALUE_FLAGS = new Set([
29
+ "--preset", "--layout", "--show", "--hide", "--order", "--first-line", "--path-levels", "--separators", "--git",
30
+ ]);
31
+ const FLAG_NAMES = new Set([
32
+ "preset", "layout", "show", "hide", "order", "first-line", "path-levels", "separators", "git", "on", "off", "reset",
33
+ ]);
34
+ const LAYOUTS = new Set(["compact", "expanded"]);
35
+ /**
36
+ * `--`를 생략한 형태를 받아 준다 — 슬래시 명령(`/pinglet-hud essential`)에서 대시를 붙이는 게
37
+ * 부자연스럽기 때문이다. `essential` → `--preset essential`, `preset essential` → `--preset essential`,
38
+ * `compact` → `--layout compact`. 값 자리(예: `--git off`)의 토큰은 그대로 둔다.
39
+ */
40
+ function normalizeHudArgs(args) {
41
+ const out = [];
42
+ for (const arg of args) {
43
+ const previous = out[out.length - 1];
44
+ if (previous !== undefined && VALUE_FLAGS.has(previous)) {
45
+ out.push(arg);
46
+ continue;
47
+ }
48
+ if (arg.startsWith("--")) {
49
+ out.push(arg);
50
+ continue;
51
+ }
52
+ const bare = arg.replace(/^-+/, "").toLowerCase();
53
+ if (FLAG_NAMES.has(bare))
54
+ out.push(`--${bare}`);
55
+ else if (PRESETS.includes(bare))
56
+ out.push("--preset", bare);
57
+ else if (LAYOUTS.has(bare))
58
+ out.push("--layout", bare);
59
+ else
60
+ out.push(arg);
61
+ }
62
+ return out;
63
+ }
64
+ function parseHudArgs(rawArgs) {
65
+ const args = normalizeHudArgs(rawArgs);
66
+ const parsed = { show: [], hide: [], reset: false, errors: [] };
67
+ for (let i = 0; i < args.length; i++) {
68
+ const arg = args[i];
69
+ const next = () => args[++i];
70
+ switch (arg) {
71
+ case "--preset": {
72
+ const value = next();
73
+ if (PRESETS.includes(value))
74
+ parsed.preset = value;
75
+ else
76
+ parsed.errors.push((0, i18n_1.t)("hud.badPreset", { value: value ?? "" }));
77
+ break;
78
+ }
79
+ case "--layout": {
80
+ const value = next();
81
+ if (value === "compact" || value === "expanded")
82
+ parsed.layout = value;
83
+ else
84
+ parsed.errors.push((0, i18n_1.t)("hud.badLayout", { value: value ?? "" }));
85
+ break;
86
+ }
87
+ case "--show":
88
+ case "--hide": {
89
+ const list = splitList(next());
90
+ for (const item of list) {
91
+ if (!config_2.TOGGLE_KEYS[item])
92
+ parsed.errors.push((0, i18n_1.t)("hud.badToggle", { value: item }));
93
+ else
94
+ (arg === "--show" ? parsed.show : parsed.hide).push(item);
95
+ }
96
+ break;
97
+ }
98
+ case "--order": {
99
+ const list = splitList(next());
100
+ const bad = list.filter((item) => !config_2.KNOWN_ELEMENTS.has(item));
101
+ if (bad.length > 0 || list.length === 0)
102
+ parsed.errors.push((0, i18n_1.t)("hud.badOrder", { value: bad.join(", ") }));
103
+ else
104
+ parsed.order = list;
105
+ break;
106
+ }
107
+ case "--first-line": {
108
+ const list = splitList(next());
109
+ const bad = list.filter((item) => !config_2.KNOWN_FIRST_LINE_SEGMENTS.has(item));
110
+ if (bad.length > 0)
111
+ parsed.errors.push((0, i18n_1.t)("hud.badFirstLine", { value: bad.join(", ") }));
112
+ else
113
+ parsed.firstLine = list;
114
+ break;
115
+ }
116
+ case "--path-levels": {
117
+ const value = next();
118
+ if (value === "full")
119
+ parsed.pathLevels = "full";
120
+ else if (value === "1" || value === "2" || value === "3")
121
+ parsed.pathLevels = Number(value);
122
+ else
123
+ parsed.errors.push((0, i18n_1.t)("hud.badPathLevels", { value: value ?? "" }));
124
+ break;
125
+ }
126
+ case "--separators": {
127
+ const value = onOff(next());
128
+ if (value === undefined)
129
+ parsed.errors.push((0, i18n_1.t)("hud.badOnOff", { flag: arg }));
130
+ else
131
+ parsed.separators = value;
132
+ break;
133
+ }
134
+ case "--git": {
135
+ const value = onOff(next());
136
+ if (value === undefined)
137
+ parsed.errors.push((0, i18n_1.t)("hud.badOnOff", { flag: arg }));
138
+ else
139
+ parsed.git = value;
140
+ break;
141
+ }
142
+ case "--on":
143
+ parsed.enabled = true;
144
+ break;
145
+ case "--off":
146
+ parsed.enabled = false;
147
+ break;
148
+ case "--reset":
149
+ parsed.reset = true;
150
+ break;
151
+ case "--quiet":
152
+ // cli가 이미 걸러 내지만, 사용자가 직접 넘긴 경우에도 오류로 만들지 않는다.
153
+ break;
154
+ default:
155
+ parsed.errors.push((0, i18n_1.t)("hud.unknownArg", { value: arg }));
156
+ }
157
+ }
158
+ return parsed;
159
+ }
160
+ function applyHudArgs(current, parsed) {
161
+ let hud = parsed.reset ? structuredClone(config_2.DEFAULT_HUD_CONFIG) : structuredClone(current);
162
+ if (parsed.preset)
163
+ hud = (0, config_2.applyPreset)(hud, parsed.preset);
164
+ if (parsed.layout)
165
+ hud.lineLayout = parsed.layout;
166
+ for (const item of parsed.show)
167
+ (0, config_2.setToggle)(hud, item, true);
168
+ for (const item of parsed.hide)
169
+ (0, config_2.setToggle)(hud, item, false);
170
+ if (parsed.order)
171
+ hud.elementOrder = parsed.order;
172
+ if (parsed.firstLine)
173
+ hud.projectLineOrder = parsed.firstLine;
174
+ if (parsed.pathLevels)
175
+ hud.pathLevels = parsed.pathLevels;
176
+ if (parsed.separators !== undefined)
177
+ hud.showSeparators = parsed.separators;
178
+ if (parsed.git !== undefined)
179
+ hud.gitStatus.enabled = parsed.git;
180
+ if (parsed.enabled !== undefined)
181
+ hud.enabled = parsed.enabled;
182
+ return (0, config_2.normalizeHudConfig)(hud);
183
+ }
184
+ /** 미리보기용 가짜 payload — 실제 세션 값이 아니라 설정이 어떻게 보이는지만 확인한다. */
185
+ const PREVIEW_PAYLOAD = {
186
+ model: { id: "claude-opus-5", display_name: "Opus 5" },
187
+ cwd: process.cwd(),
188
+ context_window: { context_window_size: 200_000, used_percentage: 45, current_usage: { input_tokens: 80_000, output_tokens: 10_000 } },
189
+ cost: { total_cost_usd: 0.42, total_duration_ms: 300_000, total_api_duration_ms: 120_000 },
190
+ rate_limits: {
191
+ five_hour: { used_percentage: 25, resets_at: Math.floor(Date.now() / 1000) + 5400 },
192
+ seven_day: { used_percentage: 10, resets_at: Math.floor(Date.now() / 1000) + 3 * 86_400 },
193
+ },
194
+ };
195
+ function summary(hud) {
196
+ const on = Object.entries(config_2.TOGGLE_KEYS)
197
+ .filter(([, key], index, all) => all.findIndex(([, k]) => k === key) === index)
198
+ .filter(([, key]) => {
199
+ const [section, field] = key.split(".");
200
+ return hud[section][field] === true;
201
+ })
202
+ .map(([name]) => name);
203
+ return [
204
+ ` enabled: ${hud.enabled ? "on" : "off"}`,
205
+ ` layout: ${hud.lineLayout}${hud.showSeparators ? " (+separators)" : ""}`,
206
+ ` order: ${hud.elementOrder.join(", ")}`,
207
+ ` first line: ${hud.projectLineOrder.length > 0 ? hud.projectLineOrder.join(", ") : "(default)"}`,
208
+ ` path levels: ${hud.pathLevels}`,
209
+ ` git: ${hud.gitStatus.enabled ? "on" : "off"}`,
210
+ ` shown: ${on.join(", ")}`,
211
+ ].join("\n");
212
+ }
213
+ async function runHud(args, options = {}) {
214
+ const config = (0, config_1.loadConfig)();
215
+ const current = (0, config_2.normalizeHudConfig)(config.hud);
216
+ const parsed = parseHudArgs(args);
217
+ if (parsed.errors.length > 0) {
218
+ for (const error of parsed.errors)
219
+ console.log(`✗ ${error}`);
220
+ console.log((0, i18n_1.t)("hud.usage"));
221
+ process.exitCode = 1;
222
+ return;
223
+ }
224
+ const changed = args.length > 0;
225
+ const next = changed ? applyHudArgs(current, parsed) : current;
226
+ if (changed) {
227
+ config.hud = next;
228
+ (0, config_1.saveConfig)(config);
229
+ }
230
+ if (options.quiet) {
231
+ console.log(changed ? (0, i18n_1.t)("hud.savedQuiet", { layout: next.lineLayout, enabled: next.enabled ? "on" : "off" }) : summary(next).replace(/\n/g, " ·").replace(/\s+/g, " ").trim());
232
+ return;
233
+ }
234
+ console.log("");
235
+ console.log(changed ? (0, i18n_1.t)("hud.saved") : (0, i18n_1.t)("hud.current"));
236
+ console.log(summary(next));
237
+ console.log("");
238
+ console.log((0, i18n_1.t)("hud.preview"));
239
+ const lines = await (0, hud_1.renderHudLines)(next, PREVIEW_PAYLOAD);
240
+ if (lines.length === 0)
241
+ console.log(` ${(0, i18n_1.t)("hud.previewEmpty")}`);
242
+ for (const line of lines)
243
+ console.log(` ${line}`);
244
+ console.log("");
245
+ if (!changed)
246
+ console.log((0, i18n_1.t)("hud.usage"));
247
+ }
@@ -49,6 +49,12 @@ async function runRefresh() {
49
49
  if (!fs.existsSync(config_1.CONFIG_PATH))
50
50
  return;
51
51
  const config = (0, config_1.loadConfig)();
52
+ try {
53
+ (0, claude_1.ensureSlashCommands)(config);
54
+ }
55
+ catch {
56
+ // 명령 파일 보충 실패는 feed 갱신을 막지 않는다.
57
+ }
52
58
  const wanted = [];
53
59
  if (config.adapters.claude)
54
60
  wanted.push("CLAUDE");