ai-battery 0.1.4 → 0.1.5

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.md CHANGED
@@ -50,8 +50,8 @@ Codex 86% │ 5h 18:09 │ 7d 82% ┃ Claude 4% │ 5h 18:10 │ 7d 71%
50
50
  | `ai-battery` | 지원 | 지원 | 지원 | 지원 | Node.js 18 이상이 필요합니다. |
51
51
  | `ai-battery --watch` | 지원 | 지원 | 지원 | 지원 | 터미널 안에서 주기적으로 갱신합니다. |
52
52
  | Claude statusLine | 지원 | 지원 | 지원 | 지원 | Claude Code `statusLine`에 `node <script>` 명령을 저장합니다. |
53
- | Codex terminal row | 미지원 | 지원 | 지원 | 지원 | `ai-battery-run`이 POSIX PTY와 `python3`를 사용합니다. |
54
- | `ai-battery setup codex` | 미지원 | 지원 | 지원 | 지원 | `codex` wrapper POSIX shell/PTY 기반입니다. |
53
+ | Codex terminal row | 지원 | 지원 | 지원 | 지원 | Windows는 `node-pty`/ConPTY가 있으면 reserved row, 없으면 overlay fallback을 사용합니다. WSL/Linux/macOS는 POSIX PTY와 `python3`를 사용합니다. |
54
+ | `ai-battery setup codex` | 지원 | 지원 | 지원 | 지원 | Windows는 `codex.cmd` wrapper, WSL/Linux/macOS는 POSIX shell wrapper를 설치합니다. |
55
55
  | `ai-battery hud` | 지원 | 지원 | 미지원 | 지원 | Windows/WSL은 PowerShell/WinForms HUD, macOS는 menu bar status item입니다. |
56
56
 
57
57
  실행 중 감지(흰색/회색 표시)는 Linux/WSL은 `/proc`, macOS는 `ps`, Windows는 PowerShell 프로세스 목록을 사용합니다.
@@ -154,7 +154,7 @@ npm uninstall -g ai-battery
154
154
 
155
155
  ## Setup
156
156
 
157
- `setup`은 한 번만 실행합니다. Claude Code에는 statusLine hook을 설치하고, Codex에는 `codex` wrapper를 설치해서 이후에는 추가 명령 없이 원래처럼 실행하게 합니다.
157
+ `setup`은 한 번만 실행합니다. Claude Code에는 statusLine hook을 설치하고, Codex에는 platform wrapper를 설치해서 이후에는 추가 명령 없이 원래처럼 실행하게 합니다.
158
158
 
159
159
  ```bash
160
160
  ai-battery setup
@@ -169,6 +169,8 @@ ai-battery setup codex
169
169
 
170
170
  Codex wrapper는 기존 `codex` 명령을 직접 덮어쓰지 않습니다. `~/.local/bin`이 이미 PATH에서 원본 `codex`보다 앞에 있고 `~/.local/bin/codex`가 비어 있거나 AI Battery 관리 파일이면 그 위치에 wrapper를 둬서 바로 잡히게 합니다. 그렇지 않으면 `~/.local/share/ai-battery/bin/codex`에 관리형 wrapper를 만들고, 필요한 경우 셸 설정에 이 디렉터리를 PATH 앞쪽으로 추가합니다. `~/.local/bin/codex` 같은 공용 위치에 이미 다른 파일이 있으면 덮어쓰지 않습니다. 새 터미널부터 `codex`가 자동으로 AI Battery 하단 행과 함께 실행됩니다. 같은 터미널에서 이미 `codex`를 실행한 적이 있으면 셸 캐시 때문에 `hash -r`이 한 번 필요할 수 있고, PATH 추가가 필요한 경우에는 `setup` 출력에 표시되는 `source ...` 명령을 실행하세요.
171
171
 
172
+ Windows native `cmd`/PowerShell에서는 `codex.cmd` wrapper가 Windows runner를 실행합니다. `node-pty`가 설치되어 있으면 ConPTY로 Codex 화면을 한 줄 짧게 띄워 하단 row를 예약하고, `node-pty`가 없거나 로드되지 않으면 같은 콘솔의 하단에 overlay row를 다시 그리는 fallback을 사용합니다. Claude statusLine은 일반 `cmd`/PowerShell 프롬프트가 아니라 Claude Code 안에서만 표시됩니다.
173
+
172
174
  Codex 하단 행이 보이지 않으면 진단을 실행합니다.
173
175
 
174
176
  ```bash
@@ -237,7 +239,7 @@ Claude가 한 번 이상 statusLine payload를 전달해야 Claude의 사용량
237
239
 
238
240
  ## Desktop HUD
239
241
 
240
- 일반 터미널 위에 외부 프로세스가 안전하게 status line을 덧그리는 방식은 안정적이지 않습니다. 그래서 Windows에서는 floating overlay를, macOS에서는 상단 menu bar status item을 제공합니다. Windows native에서는 WSL 없이 PowerShell/WinForms로 바로 실행되고, WSL에서는 `powershell.exe`를 통해 같은 HUD를 띄웁니다. macOS에서는 메뉴바 두께에 맞춰 `Cx 75% | Cl 4%`처럼 요약만 표시하고, 클릭하면 자세한 상태를 볼 수 있습니다.
242
+ 일반 터미널 위에 외부 프로세스가 안전하게 status line을 덧그리는 방식은 안정적이지 않습니다. 그래서 Windows에서는 floating overlay를, macOS에서는 상단 menu bar status item을 제공합니다. Windows native에서는 WSL 없이 PowerShell/WinForms로 바로 실행되고, WSL에서는 `powershell.exe`를 통해 같은 HUD를 띄웁니다. macOS에서는 투명 배경의 작은 SVG 이미지로 Codex와 Claude 로고, 짧은 미터, 퍼센트만 표시하고, 클릭하면 자세한 상태를 볼 수 있습니다.
241
243
 
242
244
  ```bash
243
245
  ai-battery hud
@@ -303,7 +305,7 @@ Codex는 최근 세션 로그에서 `rate_limits` 이벤트를 찾습니다. Cla
303
305
 
304
306
  ## Source Environment
305
307
 
306
- 기본 CLI는 Node.js 18 이상이 있으면 Windows native, WSL, Linux, macOS에서 실행됩니다. `ai-battery-run`과 Codex terminal row는 Python 3와 POSIX PTY가 필요해서 WSL/Linux/macOS에서 지원됩니다. HUD는 Windows/WSL에서는 PowerShell/WinForms, macOS에서는 내장 `osascript`와 AppleScriptObjC를 사용합니다.
308
+ 기본 CLI는 Node.js 18 이상이 있으면 Windows native, WSL, Linux, macOS에서 실행됩니다. Windows native의 Codex terminal row는 Node runner를 사용하며, optional `node-pty`가 있으면 ConPTY reserved row를 사용하고 없으면 overlay fallback을 사용합니다. WSL/Linux/macOS `ai-battery-run`은 Python 3와 POSIX PTY를 사용합니다. HUD는 Windows/WSL에서는 PowerShell/WinForms, macOS에서는 내장 `osascript`와 AppleScriptObjC를 사용합니다.
307
309
 
308
310
  Codex 데이터는 기본적으로 `~/.codex/sessions`를 읽습니다. 다른 위치를 쓰고 있다면 `CODEX_HOME`을 설정하세요.
309
311
 
@@ -1,89 +1,21 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
5
- SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
6
- HUD_PS1="$SCRIPT_DIR/ai-battery-hud.ps1"
7
-
8
- if ! command -v powershell.exe >/dev/null 2>&1; then
9
- echo "ai-battery-hud: powershell.exe is required for the Windows always-on-top HUD." >&2
10
- exit 1
11
- fi
12
-
13
- FOREGROUND=0
14
- ONCE=0
15
- STOP=0
16
- FILTERED_ARGS=()
17
- for arg in "$@"; do
18
- case "$arg" in
19
- -Foreground|--foreground)
20
- FOREGROUND=1
21
- ;;
22
- -Movable|--movable)
23
- FILTERED_ARGS+=("-Movable")
24
- ;;
25
- -Once|--once)
26
- ONCE=1
27
- FILTERED_ARGS+=("$arg")
28
- ;;
29
- -Stop|--stop)
30
- STOP=1
31
- FILTERED_ARGS+=("-StopExisting")
32
- ;;
33
- *)
34
- FILTERED_ARGS+=("$arg")
35
- ;;
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ script=$0
5
+ while [ -h "$script" ]; do
6
+ dir=$(CDPATH= cd -- "$(dirname -- "$script")" && pwd)
7
+ link=$(readlink "$script")
8
+ case "$link" in
9
+ /*) script=$link ;;
10
+ *) script=$dir/$link ;;
36
11
  esac
37
12
  done
38
13
 
39
- HUD_PS1_WIN="$(wslpath -w "$HUD_PS1")"
40
-
41
- if [[ -n "${CLAUDEX_BATTERY_COMMAND:-}" ]]; then
42
- AI_BATTERY_COMMAND="$CLAUDEX_BATTERY_COMMAND"
43
- fi
14
+ script_dir=$(CDPATH= cd -- "$(dirname -- "$script")" && pwd)
44
15
 
45
- if [[ -z "${AI_BATTERY_COMMAND:-}" ]]; then
46
- NODE_BIN="$(command -v node || true)"
47
- if [[ -z "$NODE_BIN" ]]; then
48
- echo "ai-battery-hud: node was not found in WSL PATH." >&2
49
- exit 1
50
- fi
51
- printf -v WSL_HOME_QUOTED "%q" "$HOME"
52
- printf -v NODE_BIN_QUOTED "%q" "$NODE_BIN"
53
- printf -v AI_BATTERY_JS_QUOTED "%q" "$SCRIPT_DIR/ai-battery.js"
54
- ENV_PREFIX=()
55
- for name in AI_BATTERY_STATE_DIR CLAUDEX_BATTERY_STATE_DIR AI_BATTERY_COLUMNS CLAUDEX_BATTERY_COLUMNS AI_BATTERY_COLUMN_GUARD CLAUDEX_BATTERY_COLUMN_GUARD CODEX_HOME; do
56
- if [[ -n "${!name:-}" ]]; then
57
- printf -v value_quoted "%q" "${!name}"
58
- ENV_PREFIX+=("$name=$value_quoted")
59
- fi
60
- done
61
- AI_BATTERY_COMMAND="${ENV_PREFIX[*]} HOME=$WSL_HOME_QUOTED $NODE_BIN_QUOTED $AI_BATTERY_JS_QUOTED --json"
62
- fi
63
-
64
- PS_ARGS=(-NoProfile -ExecutionPolicy Bypass -File "$HUD_PS1_WIN" -BatteryCommand "$AI_BATTERY_COMMAND" "${FILTERED_ARGS[@]}")
65
-
66
- if [[ "$FOREGROUND" == "1" || "$ONCE" == "1" || "$STOP" == "1" ]]; then
67
- exec powershell.exe "${PS_ARGS[@]}"
16
+ if ! command -v node >/dev/null 2>&1; then
17
+ echo "ai-battery-hud: node was not found on PATH." >&2
18
+ exit 1
68
19
  fi
69
20
 
70
- ps_quote() {
71
- local value="${1//\'/\'\'}"
72
- printf "'%s'" "$value"
73
- }
74
-
75
- win_arg_quote() {
76
- local value="${1//\"/\\\"}"
77
- printf '"%s"' "$value"
78
- }
79
-
80
- PS_CMDLINE=""
81
- for arg in "${PS_ARGS[@]}"; do
82
- if [[ -n "$PS_CMDLINE" ]]; then
83
- PS_CMDLINE+=" "
84
- fi
85
- PS_CMDLINE+="$(win_arg_quote "$arg")"
86
- done
87
-
88
- powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "Start-Process -WindowStyle Hidden -FilePath 'powershell.exe' -ArgumentList $(ps_quote "$PS_CMDLINE")" >/dev/null 2>&1
89
- echo "AI Battery HUD started or already running. Drag it to place it; right-click and choose Exit to close."
21
+ exec node "$script_dir/ai-battery-hud.js" "$@"
@@ -78,6 +78,15 @@ function sleepSync(ms) {
78
78
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
79
79
  }
80
80
 
81
+ function waitForFile(filePath, timeoutMs = 3500) {
82
+ const start = Date.now();
83
+ while (Date.now() - start < timeoutMs) {
84
+ if (fs.existsSync(filePath)) return true;
85
+ sleepSync(100);
86
+ }
87
+ return false;
88
+ }
89
+
81
90
  function snapshotHasWeekly(jsonText) {
82
91
  try {
83
92
  const snapshot = JSON.parse(jsonText);
@@ -188,7 +197,8 @@ function macCommands(options) {
188
197
  const providerArgs = macProviderArgs(options.provider);
189
198
  const envPrefix = relevantEnvPrefix();
190
199
  return {
191
- title: `${envPrefix} ${node} ${batteryJs} --menu-bar${providerArgs} 2>/dev/null`,
200
+ title: `${envPrefix} ${node} ${batteryJs} --menu-bar-image${providerArgs} 2>/dev/null`,
201
+ detailImage: `${envPrefix} ${node} ${batteryJs} --menu-detail-image${providerArgs} 2>/dev/null`,
192
202
  detail: `${envPrefix} ${node} ${batteryJs} --no-color${providerArgs} 2>/dev/null`
193
203
  };
194
204
  }
@@ -313,14 +323,16 @@ function runMacHud(cliArgs) {
313
323
  process.exit(result.status ?? 0);
314
324
  }
315
325
 
316
- if (macHudPids().length) {
317
- console.log("AI Battery menu bar is already running.");
318
- process.exit(0);
326
+ const existingPids = macHudPids();
327
+ if (existingPids.length) {
328
+ spawnSync("kill", existingPids.map(String), { stdio: "ignore" });
329
+ sleepSync(400);
319
330
  }
320
331
 
321
332
  const osascriptArgs = [
322
333
  macStatusPath,
323
334
  commands.title,
335
+ commands.detailImage,
324
336
  commands.detail,
325
337
  String(options.interval)
326
338
  ];
@@ -335,7 +347,7 @@ function runMacHud(cliArgs) {
335
347
  stdio: "ignore"
336
348
  });
337
349
  child.unref();
338
- console.log("AI Battery menu bar started. Click the menu bar item for details; use \"ai-battery hud stop\" to close it.");
350
+ console.log(`${existingPids.length ? "AI Battery menu bar restarted" : "AI Battery menu bar started"}. Click the menu bar item for details; use "ai-battery hud stop" to close it.`);
339
351
  process.exit(0);
340
352
  }
341
353
 
@@ -511,6 +523,9 @@ if (subcommand === "autostart") {
511
523
  }
512
524
 
513
525
  const initialJson = stop ? null : prefetchInitialJson(batteryCommand, useWsl);
526
+ const readyPath = (!useWsl && process.platform === "win32" && !foreground && !once && !stop)
527
+ ? path.join(os.tmpdir(), `ai-battery-hud-ready-${process.pid}-${Date.now()}.json`)
528
+ : null;
514
529
 
515
530
  const psArgs = [
516
531
  "-NoProfile",
@@ -523,6 +538,10 @@ const psArgs = [
523
538
  ...filteredArgs
524
539
  ];
525
540
 
541
+ if (readyPath) {
542
+ psArgs.push("-ReadyPath", readyPath);
543
+ }
544
+
526
545
  if (initialJson) {
527
546
  psArgs.push("-InitialJsonBase64", Buffer.from(initialJson, "utf8").toString("base64"));
528
547
  }
@@ -560,4 +579,9 @@ if (useWsl) {
560
579
  child.unref();
561
580
  }
562
581
 
563
- console.log("AI Battery HUD started or already running. Drag it to place it; right-click and choose Exit to close.");
582
+ if (readyPath && !waitForFile(readyPath)) {
583
+ console.log("AI Battery HUD start requested, but no visible window was confirmed. Run: ai-battery hud --foreground");
584
+ } else {
585
+ console.log("AI Battery HUD started. Drag it to place it; right-click and choose Exit to close.");
586
+ }
587
+ if (readyPath) fs.rmSync(readyPath, { force: true });
@@ -12,7 +12,8 @@ param(
12
12
  [switch]$ClickThrough,
13
13
  [switch]$StopExisting,
14
14
  [switch]$UseWsl,
15
- [switch]$Once
15
+ [switch]$Once,
16
+ [string]$ReadyPath = ""
16
17
  )
17
18
 
18
19
  $ErrorActionPreference = "Stop"
@@ -112,6 +113,22 @@ $script:hudAnchorY = "top"
112
113
  $script:codexRowVisible = $true
113
114
  $script:claudeRowVisible = $true
114
115
 
116
+ function Signal-HudReady {
117
+ if ([string]::IsNullOrWhiteSpace($ReadyPath)) { return }
118
+ try {
119
+ $dir = Split-Path -Parent $ReadyPath
120
+ if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
121
+ @{
122
+ Ready = $true
123
+ Pid = $PID
124
+ At = [datetime]::UtcNow.ToString("o")
125
+ Mode = $Mode
126
+ } | ConvertTo-Json -Compress | Set-Content -Encoding UTF8 -Path $ReadyPath
127
+ } catch {
128
+ # Readiness is diagnostic only.
129
+ }
130
+ }
131
+
115
132
  function Read-HudPlacement {
116
133
  foreach ($path in @((Get-HudStatePath), (Get-LegacyHudStatePath))) {
117
134
  try {
@@ -969,6 +986,7 @@ if ($Mode -eq "tray") {
969
986
  $timer.add_Tick({ Update-Tray })
970
987
  Update-Tray
971
988
  $timer.Start()
989
+ Signal-HudReady
972
990
  [System.Windows.Forms.Application]::Run($context)
973
991
  Release-SingleInstance
974
992
  exit 0
@@ -1807,6 +1825,7 @@ $form.add_SizeChanged({ Sync-HitFormBounds })
1807
1825
  $form.add_Shown({
1808
1826
  Show-HitForm
1809
1827
  Update-HudVisibilityForFullscreen
1828
+ Signal-HudReady
1810
1829
  })
1811
1830
 
1812
1831
  Set-Position
@@ -4,9 +4,11 @@ use scripting additions
4
4
 
5
5
  property statusItem : missing value
6
6
  property detailItem : missing value
7
+ property detailView : missing value
7
8
  property refreshTimer : missing value
8
9
  property titleCommand : ""
9
10
  property detailCommand : ""
11
+ property tooltipCommand : ""
10
12
  property refreshInterval : 10
11
13
 
12
14
  on run argv
@@ -14,10 +16,18 @@ on run argv
14
16
 
15
17
  set titleCommand to item 1 of argv
16
18
  set detailCommand to item 2 of argv
17
- if (count of argv) is greater than or equal to 3 then
19
+ set tooltipCommand to detailCommand
20
+ if (count of argv) is greater than or equal to 4 then
21
+ set tooltipCommand to item 3 of argv
18
22
  try
19
- set refreshInterval to (item 3 of argv) as real
23
+ set refreshInterval to (item 4 of argv) as real
20
24
  end try
25
+ else
26
+ if (count of argv) is greater than or equal to 3 then
27
+ try
28
+ set refreshInterval to (item 3 of argv) as real
29
+ end try
30
+ end if
21
31
  end if
22
32
 
23
33
  current application's NSApplication's sharedApplication()
@@ -25,6 +35,7 @@ on run argv
25
35
 
26
36
  set statusItem to current application's NSStatusBar's systemStatusBar()'s statusItemWithLength:(current application's NSVariableStatusItemLength)
27
37
  statusItem's button()'s setTitle:"AI --"
38
+ statusItem's button()'s setImagePosition:(current application's NSNoImage)
28
39
  statusItem's button()'s setToolTip:"AI Battery"
29
40
 
30
41
  set statusMenu to current application's NSMenu's alloc()'s initWithTitle:"AI Battery"
@@ -46,27 +57,72 @@ on run argv
46
57
 
47
58
  set refreshTimer to current application's NSTimer's scheduledTimerWithTimeInterval:refreshInterval target:me selector:"refresh:" userInfo:(missing value) repeats:true
48
59
  current application's NSRunLoop's currentRunLoop()'s addTimer:refreshTimer forMode:(current application's NSRunLoopCommonModes)
49
- current application's NSApp's run()
60
+ current application's NSApp's |run|()
50
61
  end run
51
62
 
52
63
  on refresh_(sender)
53
64
  set titleText to "AI --"
54
65
  set detailText to "AI Battery unavailable"
66
+ set tooltipText to "AI Battery unavailable"
67
+ set imagePath to ""
68
+ set menuImage to missing value
69
+ set detailImage to missing value
55
70
 
56
71
  try
57
- set titleText to do shell script titleCommand
72
+ set imagePath to (do shell script (titleCommand as text))
58
73
  end try
59
74
 
60
75
  try
61
- set detailText to do shell script detailCommand
76
+ set detailText to (do shell script (detailCommand as text))
62
77
  end try
63
78
 
64
- if titleText is "" then set titleText to "AI --"
79
+ try
80
+ set tooltipText to (do shell script (tooltipCommand as text))
81
+ end try
82
+
83
+ if imagePath is "" then set imagePath to "AI --"
65
84
  if detailText is "" then set detailText to "AI Battery unavailable"
85
+ if tooltipText is "" then set tooltipText to detailText
86
+
87
+ try
88
+ set menuImage to current application's NSImage's alloc()'s initWithContentsOfFile:imagePath
89
+ end try
90
+
91
+ try
92
+ set detailImage to current application's NSImage's alloc()'s initWithContentsOfFile:detailText
93
+ end try
94
+
95
+ if menuImage is not missing value then
96
+ set imageSize to menuImage's |size|()
97
+ statusItem's setLength:(width of imageSize)
98
+ menuImage's setTemplate:false
99
+ statusItem's button()'s setImage:menuImage
100
+ statusItem's button()'s setImageScaling:(current application's NSImageScaleProportionallyDown)
101
+ statusItem's button()'s setImagePosition:(current application's NSImageOnly)
102
+ statusItem's button()'s setTitle:""
103
+ else
104
+ set titleText to imagePath
105
+ if titleText is "" then set titleText to "AI --"
106
+ statusItem's setLength:(current application's NSVariableStatusItemLength)
107
+ statusItem's button()'s setImage:(missing value)
108
+ statusItem's button()'s setImagePosition:(current application's NSNoImage)
109
+ statusItem's button()'s setTitle:titleText
110
+ end if
111
+
112
+ if detailImage is not missing value then
113
+ set detailSize to detailImage's |size|()
114
+ set detailFrame to current application's NSMakeRect(0, 0, (width of detailSize), (height of detailSize))
115
+ set detailView to current application's NSImageView's alloc()'s initWithFrame:detailFrame
116
+ detailView's setImage:detailImage
117
+ detailView's setImageScaling:(current application's NSImageScaleNone)
118
+ detailItem's setView:detailView
119
+ detailItem's setTitle:""
120
+ else
121
+ detailItem's setView:(missing value)
122
+ detailItem's setTitle:detailText
123
+ end if
66
124
 
67
- statusItem's button()'s setTitle:titleText
68
- statusItem's button()'s setToolTip:detailText
69
- detailItem's setTitle:detailText
125
+ statusItem's button()'s setToolTip:tooltipText
70
126
  end refresh_
71
127
 
72
128
  on quit_(sender)