ai-battery 0.1.3 → 0.1.4

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
@@ -107,6 +107,7 @@ ai-battery --version
107
107
  ai-battery --provider codex
108
108
  ai-battery --provider claude
109
109
  ai-battery setup
110
+ ai-battery uninstall
110
111
  ai-battery doctor
111
112
  ai-battery hud
112
113
  ai-battery off codex
@@ -124,6 +125,33 @@ ai-battery on codex
124
125
 
125
126
  `doctor`는 설치 상태와 함께 npm latest 버전을 확인합니다. 네트워크가 막혀 있으면 버전 확인만 건너뛰고 나머지 진단은 계속 표시합니다.
126
127
 
128
+ ## Uninstall
129
+
130
+ `off`는 표시만 숨기는 설정이고, `uninstall`은 `setup`과 HUD autostart가 만든 통합 지점을 제거합니다.
131
+
132
+ ```bash
133
+ ai-battery uninstall
134
+ ```
135
+
136
+ 일부만 제거할 수도 있습니다.
137
+
138
+ ```bash
139
+ ai-battery uninstall codex
140
+ ai-battery uninstall claude
141
+ ai-battery uninstall hud
142
+ ```
143
+
144
+ 이 명령은 AI Battery가 관리 마커를 넣은 Codex wrapper, Claude `statusLine`, HUD/menu bar autostart와 실행 중인 HUD를 정리합니다. 다른 도구가 만든 `codex` 파일이나 Claude `statusLine`은 건드리지 않습니다. 이전 버전이나 `--force`로 기존 파일을 백업한 경우에는 가능한 한 원래 파일/symlink를 복원합니다. 현재 이미 AI Battery wrapper 안에서 실행 중인 Codex 세션의 terminal row는 그 세션을 종료해야 사라집니다.
145
+
146
+ 최신 npm은 패키지의 uninstall lifecycle을 실행하지 않기 때문에 `npm uninstall ai-battery` 또는 `npm uninstall -g ai-battery`만으로는 외부 통합 지점을 자동 정리할 수 없습니다. 완전히 제거하려면 npm 패키지를 지우기 전에 먼저 실행하세요.
147
+
148
+ ```bash
149
+ ai-battery uninstall
150
+ npm uninstall -g ai-battery
151
+ ```
152
+
153
+ 이미 npm 패키지를 먼저 지운 뒤라면, 다시 설치한 다음 `ai-battery uninstall`을 실행하거나 아래 항목을 직접 확인해 제거해야 합니다: AI Battery가 만든 Codex wrapper, shell rc의 `# >>> ai-battery setup >>>` block, Claude `statusLine`, HUD/menu bar autostart.
154
+
127
155
  ## Setup
128
156
 
129
157
  `setup`은 한 번만 실행합니다. Claude Code에는 statusLine hook을 설치하고, Codex에는 `codex` wrapper를 설치해서 이후에는 추가 명령 없이 원래처럼 실행하게 합니다.
@@ -139,7 +167,7 @@ ai-battery setup claude
139
167
  ai-battery setup codex
140
168
  ```
141
169
 
142
- Codex wrapper는 기존 `codex` 명령을 직접 덮어쓰지 않습니다. `~/.local/bin/codex`에 관리형 wrapper를 만들고, 필요한 경우 셸 설정에 `~/.local/bin`을 PATH 앞쪽으로 추가합니다. 새 터미널부터 `codex`가 자동으로 AI Battery 하단 행과 함께 실행됩니다. 현재 터미널에서 바로 쓰려면 `setup` 출력에 표시되는 `source ...` 명령을 실행하세요.
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 ...` 명령을 실행하세요.
143
171
 
144
172
  Codex 하단 행이 보이지 않으면 진단을 실행합니다.
145
173
 
package/bin/ai-battery.js CHANGED
@@ -18,6 +18,8 @@ const DEFAULT_STATUSLINE_COLUMN_GUARD = 4;
18
18
  const COMMANDS = new Set([
19
19
  "show",
20
20
  "setup",
21
+ "teardown",
22
+ "uninstall",
21
23
  "doctor",
22
24
  "hud",
23
25
  "on",
@@ -27,7 +29,11 @@ const COMMANDS = new Set([
27
29
  "uninstall-claude-statusline"
28
30
  ]);
29
31
  const PROVIDERS = ["codex", "claude"];
32
+ const TEARDOWN_TARGETS = ["codex", "claude", "hud"];
30
33
  const CODEX_WRAPPER_MARKER = "AI_BATTERY_MANAGED_CODEX_WRAPPER";
34
+ const CODEX_PREFERRED_BACKUP_SUFFIX = ".ai-battery-original-link";
35
+ const CODEX_TIMESTAMP_BACKUP_MARKER = ".ai-battery-backup-";
36
+ const DEFAULT_STATUSLINE_HEADER_COLUMN_GUARD = 2;
31
37
 
32
38
  function parseArgs(argv) {
33
39
  const args = {
@@ -60,7 +66,7 @@ function parseArgs(argv) {
60
66
  args.rest = argv.slice(i + 1);
61
67
  break;
62
68
  }
63
- } else if (!arg.startsWith("-") && ["setup", "on", "off"].includes(args.command)) {
69
+ } else if (!arg.startsWith("-") && ["setup", "teardown", "uninstall", "on", "off"].includes(args.command)) {
64
70
  args.targets.push(arg);
65
71
  } else if (arg === "--provider" || arg === "-p") {
66
72
  args.provider = argv[++i] || "all";
@@ -126,6 +132,7 @@ function printHelp() {
126
132
  Usage:
127
133
  ai-battery [options]
128
134
  ai-battery setup [codex|claude] [--force]
135
+ ai-battery uninstall [codex|claude|hud|all]
129
136
  ai-battery doctor
130
137
  ai-battery hud [start|stop|status] [hud options]
131
138
  ai-battery hud autostart on|off|status
@@ -157,6 +164,7 @@ Options:
157
164
  Compatibility:
158
165
  ai-battery install-claude-statusline [--force]
159
166
  ai-battery uninstall-claude-statusline
167
+ ai-battery teardown [codex|claude|hud|all]
160
168
  `);
161
169
  }
162
170
 
@@ -185,6 +193,12 @@ function stateDir() {
185
193
  return homePath(".local", "state", "ai-battery");
186
194
  }
187
195
 
196
+ function dataDir() {
197
+ if (process.env.AI_BATTERY_DATA_DIR) return process.env.AI_BATTERY_DATA_DIR;
198
+ if (process.env.XDG_DATA_HOME) return path.join(process.env.XDG_DATA_HOME, "ai-battery");
199
+ return homePath(".local", "share", "ai-battery");
200
+ }
201
+
188
202
  function legacyStateDir() {
189
203
  if (process.env.XDG_STATE_HOME) return path.join(process.env.XDG_STATE_HOME, "claudex-battery");
190
204
  return homePath(".local", "state", "claudex-battery");
@@ -217,7 +231,8 @@ function defaultConfig() {
217
231
  codex: true,
218
232
  claude: true
219
233
  },
220
- codexWrapper: null
234
+ codexWrapper: null,
235
+ claudeStatusLineBackup: null
221
236
  };
222
237
  }
223
238
 
@@ -258,6 +273,21 @@ function providerTargets(targets) {
258
273
  return [...providers];
259
274
  }
260
275
 
276
+ function teardownTargets(targets) {
277
+ const requested = targets.length ? targets : ["all"];
278
+ const selected = new Set();
279
+ for (const target of requested) {
280
+ if (target === "all") {
281
+ TEARDOWN_TARGETS.forEach((item) => selected.add(item));
282
+ } else if (TEARDOWN_TARGETS.includes(target)) {
283
+ selected.add(target);
284
+ } else {
285
+ throw new Error("target must be one of: codex, claude, hud, all");
286
+ }
287
+ }
288
+ return [...selected];
289
+ }
290
+
261
291
  function setProviderVisibility(targets, visible) {
262
292
  const providers = providerTargets(targets);
263
293
  const config = readConfig();
@@ -467,7 +497,7 @@ function codexWrapperScript(originalCommand) {
467
497
  # ${CODEX_WRAPPER_MARKER}=1
468
498
  export AI_BATTERY_ORIGINAL_CODEX=${shQuote(originalCommand)}
469
499
  export AI_BATTERY_WRAPPED_CODEX=1
470
- if [ -t 0 ] && [ -t 1 ]; then
500
+ if [ -t 0 ] && [ -t 1 ] && [ -x ${shQuote(runner)} ]; then
471
501
  exec ${shQuote(runner)} --provider all -- ${shQuote(originalCommand)} "$@"
472
502
  fi
473
503
  exec ${shQuote(originalCommand)} "$@"
@@ -482,6 +512,86 @@ function managedCodexWrapper(filePath) {
482
512
  }
483
513
  }
484
514
 
515
+ function defaultCodexShimDir() {
516
+ return path.join(dataDir(), "bin");
517
+ }
518
+
519
+ function legacyCodexWrapperPath() {
520
+ return homePath(".local", "bin", "codex");
521
+ }
522
+
523
+ function legacyCodexShimDir() {
524
+ return path.dirname(legacyCodexWrapperPath());
525
+ }
526
+
527
+ function pathIndexForDir(dir) {
528
+ return pathEntries().findIndex((entry) => path.resolve(entry) === path.resolve(dir));
529
+ }
530
+
531
+ function pathDirPrecedesOriginal(dir, originalCommand) {
532
+ const shimIndex = pathIndexForDir(dir);
533
+ if (shimIndex < 0) return false;
534
+ const originalIndex = pathIndexForDir(path.dirname(originalCommand));
535
+ return originalIndex < 0 || shimIndex < originalIndex;
536
+ }
537
+
538
+ function codexInstallSkipPaths(config = readConfig()) {
539
+ return uniquePaths([
540
+ config.codexWrapper?.wrapperPath,
541
+ process.env.AI_BATTERY_SHIM_DIR ? path.join(process.env.AI_BATTERY_SHIM_DIR, "codex") : null,
542
+ path.join(defaultCodexShimDir(), "codex"),
543
+ legacyCodexWrapperPath()
544
+ ]);
545
+ }
546
+
547
+ function canUseCodexShimTarget(wrapperPath, originalCommand) {
548
+ if (samePath(wrapperPath, originalCommand)) return false;
549
+ if (!fs.existsSync(wrapperPath)) return true;
550
+ return managedCodexWrapper(wrapperPath);
551
+ }
552
+
553
+ function selectCodexShimDir(originalCommand, pathCommand = originalCommand) {
554
+ if (process.env.AI_BATTERY_SHIM_DIR) {
555
+ return {
556
+ shimDir: process.env.AI_BATTERY_SHIM_DIR,
557
+ immediate: pathDirPrecedesOriginal(process.env.AI_BATTERY_SHIM_DIR, pathCommand),
558
+ reason: "AI_BATTERY_SHIM_DIR"
559
+ };
560
+ }
561
+
562
+ const legacyDir = legacyCodexShimDir();
563
+ const legacyWrapper = path.join(legacyDir, "codex");
564
+ if (
565
+ pathDirPrecedesOriginal(legacyDir, pathCommand)
566
+ && canUseCodexShimTarget(legacyWrapper, originalCommand)
567
+ ) {
568
+ return {
569
+ shimDir: legacyDir,
570
+ immediate: true,
571
+ reason: `${legacyDir} is already before the original codex on PATH`
572
+ };
573
+ }
574
+
575
+ const shimDir = defaultCodexShimDir();
576
+ return {
577
+ shimDir,
578
+ immediate: pathDirPrecedesOriginal(shimDir, pathCommand),
579
+ reason: "AI Battery-owned data directory"
580
+ };
581
+ }
582
+
583
+ function findOriginalCodexCommand(skipPaths = []) {
584
+ const skips = skipPaths.filter(Boolean);
585
+ for (const dir of pathEntries()) {
586
+ const candidate = path.join(dir, "codex");
587
+ if (skips.some((skip) => samePath(candidate, skip))) continue;
588
+ if (!safeStat(candidate)?.isFile() || !isExecutable(candidate)) continue;
589
+ if (managedCodexWrapper(candidate)) continue;
590
+ return candidate;
591
+ }
592
+ return null;
593
+ }
594
+
485
595
  function shellRcPath() {
486
596
  if (process.env.AI_BATTERY_RC) return process.env.AI_BATTERY_RC;
487
597
  const shell = path.basename(process.env.SHELL || "");
@@ -500,6 +610,50 @@ function shellPathBlock(shimDir) {
500
610
  return `\n# >>> ai-battery setup >>>\nexport PATH=${shQuote(shimDir)}:"$PATH"\n# <<< ai-battery setup <<<\n`;
501
611
  }
502
612
 
613
+ function removeAiBatteryShellPathBlock(text) {
614
+ return String(text).replace(
615
+ /(\r?\n)?# >>> ai-battery setup >>>\r?\n[\s\S]*?# <<< ai-battery setup <<<(?:\r?\n)?/g,
616
+ (match, leadingNewline, offset) => (offset === 0 ? "" : (leadingNewline || "\n"))
617
+ );
618
+ }
619
+
620
+ function uniquePaths(paths) {
621
+ const seen = new Set();
622
+ const result = [];
623
+ for (const filePath of paths.filter(Boolean)) {
624
+ const key = path.resolve(filePath);
625
+ if (seen.has(key)) continue;
626
+ seen.add(key);
627
+ result.push(filePath);
628
+ }
629
+ return result;
630
+ }
631
+
632
+ function shellRcCandidates() {
633
+ return uniquePaths([
634
+ process.env.AI_BATTERY_RC,
635
+ shellRcPath(),
636
+ homePath(".zshrc"),
637
+ homePath(".bashrc"),
638
+ homePath(".bash_profile"),
639
+ homePath(".profile"),
640
+ homePath(".config", "fish", "config.fish")
641
+ ]);
642
+ }
643
+
644
+ function removeShellPathBlocks() {
645
+ const changed = [];
646
+ for (const rcPath of shellRcCandidates()) {
647
+ if (!fs.existsSync(rcPath)) continue;
648
+ const existing = fs.readFileSync(rcPath, "utf8");
649
+ const next = removeAiBatteryShellPathBlock(existing);
650
+ if (next === existing) continue;
651
+ fs.writeFileSync(rcPath, next, "utf8");
652
+ changed.push(rcPath);
653
+ }
654
+ return changed;
655
+ }
656
+
503
657
  function ensureShimPath(shimDir, originalCommand) {
504
658
  const entries = pathEntries();
505
659
  const shimIndex = entries.findIndex((entry) => path.resolve(entry) === path.resolve(shimDir));
@@ -510,26 +664,159 @@ function ensureShimPath(shimDir, originalCommand) {
510
664
  return {
511
665
  changed: false,
512
666
  rcPath: null,
513
- note: `${shimDir} is already before the original codex on PATH`
667
+ note: `${shimDir} is already before the original codex on PATH. Plain "codex" can use AI Battery in new command lookups. If this shell already cached codex, run "hash -r" once.`
514
668
  };
515
669
  }
516
670
 
517
671
  const rcPath = shellRcPath();
518
672
  fs.mkdirSync(path.dirname(rcPath), { recursive: true });
519
673
  const existing = fs.existsSync(rcPath) ? fs.readFileSync(rcPath, "utf8") : "";
520
- if (!existing.includes(">>> ai-battery setup >>>")) {
521
- fs.appendFileSync(rcPath, shellPathBlock(shimDir));
674
+ const withoutAiBatteryBlock = removeAiBatteryShellPathBlock(existing);
675
+ const separator = withoutAiBatteryBlock && !withoutAiBatteryBlock.endsWith("\n") ? "\n" : "";
676
+ const next = `${withoutAiBatteryBlock}${separator}${shellPathBlock(shimDir)}`;
677
+ fs.writeFileSync(rcPath, next, "utf8");
678
+
679
+ return {
680
+ changed: true,
681
+ rcPath,
682
+ note: `${existing === withoutAiBatteryBlock ? "Added" : "Updated"} ${shimDir} before PATH in ${rcPath}. Open a new terminal for plain "codex" to use AI Battery.`
683
+ };
684
+ }
685
+
686
+ function codexWrapperCandidates(config = readConfig()) {
687
+ const activeCodex = findCommand("codex");
688
+ return uniquePaths([
689
+ config.codexWrapper?.wrapperPath,
690
+ process.env.AI_BATTERY_SHIM_DIR ? path.join(process.env.AI_BATTERY_SHIM_DIR, "codex") : null,
691
+ path.join(defaultCodexShimDir(), "codex"),
692
+ legacyCodexWrapperPath(),
693
+ activeCodex && managedCodexWrapper(activeCodex) ? activeCodex : null
694
+ ]);
695
+ }
696
+
697
+ function codexTimestampBackups(wrapperPath) {
698
+ const dir = path.dirname(wrapperPath);
699
+ const prefix = `${path.basename(wrapperPath)}${CODEX_TIMESTAMP_BACKUP_MARKER}`;
700
+ try {
701
+ return fs.readdirSync(dir)
702
+ .filter((entry) => entry.startsWith(prefix))
703
+ .map((entry) => path.join(dir, entry))
704
+ .filter((entryPath) => fs.existsSync(entryPath))
705
+ .sort((left, right) => {
706
+ const leftStat = safeStat(left);
707
+ const rightStat = safeStat(right);
708
+ return (rightStat?.mtimeMs ?? 0) - (leftStat?.mtimeMs ?? 0);
709
+ });
710
+ } catch {
711
+ return [];
712
+ }
713
+ }
714
+
715
+ function codexWrapperBackupCandidates(wrapperPath, config = readConfig()) {
716
+ return uniquePaths([
717
+ config.codexWrapper?.wrapperPath === wrapperPath ? config.codexWrapper?.backupPath : null,
718
+ `${wrapperPath}${CODEX_PREFERRED_BACKUP_SUFFIX}`,
719
+ ...codexTimestampBackups(wrapperPath)
720
+ ]);
721
+ }
722
+
723
+ function existingCodexWrapperBackup(wrapperPath, config = readConfig()) {
724
+ return codexWrapperBackupCandidates(wrapperPath, config).find((backupPath) => fs.existsSync(backupPath)) || null;
725
+ }
726
+
727
+ function nextCodexBackupPath(wrapperPath) {
728
+ const preferred = `${wrapperPath}${CODEX_PREFERRED_BACKUP_SUFFIX}`;
729
+ if (!fs.existsSync(preferred)) return preferred;
730
+ return `${wrapperPath}${CODEX_TIMESTAMP_BACKUP_MARKER}${Date.now()}`;
731
+ }
732
+
733
+ function sameOrInsidePath(childPath, parentPath) {
734
+ const child = path.resolve(childPath);
735
+ const parent = path.resolve(parentPath);
736
+ return child === parent || child.startsWith(`${parent}${path.sep}`);
737
+ }
738
+
739
+ function canRemoveCodexWrapperWithoutBackup(wrapperPath, config = readConfig()) {
740
+ if (sameOrInsidePath(wrapperPath, defaultCodexShimDir())) return true;
741
+ if (samePath(wrapperPath, legacyCodexWrapperPath())) return true;
742
+ const configuredWrapper = config.codexWrapper?.wrapperPath;
743
+ if (
744
+ configuredWrapper
745
+ && samePath(wrapperPath, configuredWrapper)
746
+ && sameOrInsidePath(wrapperPath, dataDir())
747
+ ) {
748
+ return true;
749
+ }
750
+ return false;
751
+ }
752
+
753
+ function removeOrRestoreCodexWrapper(wrapperPath, config) {
754
+ const backupPath = existingCodexWrapperBackup(wrapperPath, config);
755
+ if (!backupPath && !canRemoveCodexWrapperWithoutBackup(wrapperPath, config)) {
522
756
  return {
523
- changed: true,
524
- rcPath,
525
- note: `Added ${shimDir} before PATH in ${rcPath}. Open a new terminal for plain "codex" to use AI Battery.`
757
+ wrapperPath,
758
+ restoredFrom: null,
759
+ skipped: true,
760
+ reason: "Managed wrapper is outside AI Battery-owned paths and no backup is available for restore"
761
+ };
762
+ }
763
+ fs.rmSync(wrapperPath, { force: true });
764
+ if (backupPath) {
765
+ fs.renameSync(backupPath, wrapperPath);
766
+ return {
767
+ wrapperPath,
768
+ restoredFrom: backupPath
526
769
  };
527
770
  }
771
+ return {
772
+ wrapperPath,
773
+ restoredFrom: null
774
+ };
775
+ }
776
+
777
+ function uninstallCodexWrapper() {
778
+ const config = readConfig();
779
+ const candidates = codexWrapperCandidates(config);
780
+ const unmanaged = [];
781
+ const skippedWrappers = [];
782
+ const removedWrappers = [];
783
+ const restoredWrappers = [];
784
+
785
+ for (const wrapperPath of candidates) {
786
+ if (!fs.existsSync(wrapperPath)) continue;
787
+ if (!managedCodexWrapper(wrapperPath)) {
788
+ unmanaged.push(wrapperPath);
789
+ continue;
790
+ }
791
+ const result = removeOrRestoreCodexWrapper(wrapperPath, config);
792
+ if (result.skipped) {
793
+ skippedWrappers.push(result);
794
+ } else if (result.restoredFrom) {
795
+ restoredWrappers.push(result);
796
+ } else {
797
+ removedWrappers.push(result.wrapperPath);
798
+ }
799
+ }
800
+
801
+ const rcPaths = removeShellPathBlocks();
802
+ const hadConfig = Boolean(config.codexWrapper);
803
+ if (hadConfig) {
804
+ config.codexWrapper = null;
805
+ writeConfig(config);
806
+ }
528
807
 
529
808
  return {
530
- changed: false,
531
- rcPath,
532
- note: `${rcPath} already has an AI Battery PATH block. Open a new terminal if plain "codex" does not use AI Battery yet.`
809
+ changed: Boolean(removedWrappers.length || restoredWrappers.length || rcPaths.length || hadConfig),
810
+ wrapperPath: restoredWrappers[0]?.wrapperPath || removedWrappers[0] || null,
811
+ removedWrappers,
812
+ restoredWrappers,
813
+ skippedWrappers,
814
+ rcPaths,
815
+ configPath: hadConfig ? configPath() : null,
816
+ unmanaged,
817
+ reason: (removedWrappers.length || restoredWrappers.length)
818
+ ? null
819
+ : (skippedWrappers.length ? "Managed Codex wrapper found but left untouched because no restore backup was available" : "No managed Codex wrapper found")
533
820
  };
534
821
  }
535
822
 
@@ -542,13 +829,13 @@ function installCodexWrapper(args) {
542
829
  };
543
830
  }
544
831
 
545
- const shimDir = process.env.AI_BATTERY_SHIM_DIR || homePath(".local", "bin");
546
- const wrapperPath = path.join(shimDir, "codex");
547
832
  const config = readConfig();
548
833
  const configuredOriginal = config.codexWrapper?.originalCommand;
549
- const originalCandidate = configuredOriginal && fs.existsSync(configuredOriginal)
834
+ const discoveredOriginal = findOriginalCodexCommand(codexInstallSkipPaths(config));
835
+ const originalCandidate = configuredOriginal && fs.existsSync(configuredOriginal) && !managedCodexWrapper(configuredOriginal)
550
836
  ? configuredOriginal
551
- : findCommand("codex", [wrapperPath]);
837
+ : discoveredOriginal;
838
+ const originalPathForPath = discoveredOriginal || originalCandidate;
552
839
  const originalCommand = originalCandidate ? executableTarget(originalCandidate) : null;
553
840
 
554
841
  if (!originalCommand) {
@@ -559,20 +846,42 @@ function installCodexWrapper(args) {
559
846
  };
560
847
  }
561
848
 
849
+ const shimSelection = selectCodexShimDir(originalCommand, originalPathForPath);
850
+ const shimDir = shimSelection.shimDir;
851
+ const wrapperPath = path.join(shimDir, "codex");
852
+
853
+ if (samePath(wrapperPath, originalCommand)) {
854
+ return {
855
+ ok: false,
856
+ skipped: true,
857
+ reason: `Refusing to replace the original codex command at ${wrapperPath}`
858
+ };
859
+ }
860
+
562
861
  fs.mkdirSync(shimDir, { recursive: true, mode: 0o755 });
862
+ let backupPath = null;
563
863
  if (fs.existsSync(wrapperPath) && !managedCodexWrapper(wrapperPath)) {
564
- if (!args.force) {
565
- throw new Error(`${wrapperPath} already exists. Re-run setup with --force to replace it.`);
566
- }
567
- fs.renameSync(wrapperPath, `${wrapperPath}.ai-battery-backup-${Date.now()}`);
864
+ return {
865
+ ok: false,
866
+ skipped: true,
867
+ reason: `Refusing to replace unmanaged codex command at ${wrapperPath}. Set AI_BATTERY_SHIM_DIR to an empty AI Battery-owned directory.`
868
+ };
568
869
  }
569
870
 
570
871
  fs.writeFileSync(wrapperPath, codexWrapperScript(originalCommand), { mode: 0o755 });
571
872
 
572
- const pathResult = ensureShimPath(shimDir, originalCommand);
873
+ const wrapperCleanup = [];
874
+ for (const staleWrapperPath of codexInstallSkipPaths(config)) {
875
+ if (samePath(staleWrapperPath, wrapperPath)) continue;
876
+ if (!fs.existsSync(staleWrapperPath) || !managedCodexWrapper(staleWrapperPath)) continue;
877
+ wrapperCleanup.push(removeOrRestoreCodexWrapper(staleWrapperPath, config));
878
+ }
879
+
880
+ const pathResult = ensureShimPath(shimDir, originalPathForPath || originalCommand);
573
881
  config.codexWrapper = {
574
882
  wrapperPath,
575
883
  originalCommand,
884
+ backupPath,
576
885
  installedAt: new Date().toISOString()
577
886
  };
578
887
  writeConfig(config);
@@ -581,6 +890,10 @@ function installCodexWrapper(args) {
581
890
  ok: true,
582
891
  wrapperPath,
583
892
  originalCommand,
893
+ backupPath,
894
+ legacyCleanup: wrapperCleanup,
895
+ wrapperCleanup,
896
+ shimSelection,
584
897
  path: pathResult
585
898
  };
586
899
  }
@@ -599,11 +912,12 @@ function codexRestartNote() {
599
912
 
600
913
  function diagnoseCodex() {
601
914
  const config = readConfig();
602
- const configuredWrapper = config.codexWrapper?.wrapperPath || homePath(".local", "bin", "codex");
915
+ const configuredWrapper = config.codexWrapper?.wrapperPath || path.join(defaultCodexShimDir(), "codex");
603
916
  const configuredOriginal = config.codexWrapper?.originalCommand || null;
604
917
  const activeCodex = findCommand("codex");
605
918
  const wrapperInstalled = configuredWrapper ? managedCodexWrapper(configuredWrapper) : false;
606
919
  const activeIsWrapper = activeCodex ? managedCodexWrapper(activeCodex) : false;
920
+ const activeWrapperBackup = activeIsWrapper ? existingCodexWrapperBackup(activeCodex, config) : null;
607
921
  const originalExists = configuredOriginal ? fs.existsSync(configuredOriginal) : false;
608
922
  const runnerPath = path.join(scriptDir(), "ai-battery-run");
609
923
  const runnerExists = isExecutable(runnerPath);
@@ -617,8 +931,14 @@ function diagnoseCodex() {
617
931
  if (!wrapperInstalled) {
618
932
  notes.push("Codex wrapper is not installed. Run: ai-battery setup codex");
619
933
  }
934
+ if (activeIsWrapper && !wrapperInstalled) {
935
+ notes.push("Plain \"codex\" still resolves to an AI Battery wrapper outside the configured shim. Run: ai-battery uninstall codex");
936
+ }
937
+ if (activeWrapperBackup) {
938
+ notes.push(`A Codex backup is available for restore: ${activeWrapperBackup}`);
939
+ }
620
940
  if (wrapperInstalled && !activeIsWrapper) {
621
- notes.push("Plain \"codex\" does not resolve to the AI Battery wrapper in this shell. Open a new terminal or put ~/.local/bin before the original codex on PATH.");
941
+ notes.push(`Plain "codex" does not resolve to the AI Battery wrapper in this shell. Ensure ${path.dirname(configuredWrapper)} is before the original codex on PATH, then open a new terminal or run "hash -r" in the parent shell.`);
622
942
  }
623
943
  if (!runnerExists) {
624
944
  notes.push(`AI Battery runner is missing or not executable: ${runnerPath}`);
@@ -636,6 +956,7 @@ function diagnoseCodex() {
636
956
  providerEnabled,
637
957
  activeCodex,
638
958
  activeIsWrapper,
959
+ activeWrapperBackup,
639
960
  wrapperPath: configuredWrapper,
640
961
  wrapperInstalled,
641
962
  runnerPath,
@@ -680,7 +1001,7 @@ function runSetup(args) {
680
1001
  const targets = providerTargets(args.targets);
681
1002
  const results = {};
682
1003
  if (targets.includes("claude")) {
683
- results.claude = installClaudeStatusline({ ...args, force: true });
1004
+ results.claude = installClaudeStatusline(args);
684
1005
  }
685
1006
  if (targets.includes("codex")) {
686
1007
  results.codex = installCodexWrapper(args);
@@ -688,6 +1009,16 @@ function runSetup(args) {
688
1009
  return results;
689
1010
  }
690
1011
 
1012
+ function isWsl() {
1013
+ if (process.platform !== "linux") return false;
1014
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
1015
+ try {
1016
+ return fs.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
1017
+ } catch {
1018
+ return false;
1019
+ }
1020
+ }
1021
+
691
1022
  function runHud(args) {
692
1023
  const hudPath = path.join(scriptDir(), "ai-battery-hud.js");
693
1024
  return spawnSync(process.execPath, [hudPath, ...args.rest], {
@@ -696,6 +1027,71 @@ function runHud(args) {
696
1027
  });
697
1028
  }
698
1029
 
1030
+ function desktopHudSupported() {
1031
+ return process.platform === "darwin" || process.platform === "win32" || isWsl();
1032
+ }
1033
+
1034
+ function childResult(result) {
1035
+ return {
1036
+ ok: !result.error && result.status === 0,
1037
+ status: result.status ?? null,
1038
+ error: result.error?.message ?? null,
1039
+ stdout: (result.stdout || "").trim(),
1040
+ stderr: (result.stderr || "").trim()
1041
+ };
1042
+ }
1043
+
1044
+ function runHudCommand(args) {
1045
+ const hudPath = path.join(scriptDir(), "ai-battery-hud.js");
1046
+ return childResult(spawnSync(process.execPath, [hudPath, ...args], {
1047
+ encoding: "utf8",
1048
+ stdio: ["ignore", "pipe", "pipe"],
1049
+ timeout: 5000,
1050
+ windowsHide: true
1051
+ }));
1052
+ }
1053
+
1054
+ function uninstallHud() {
1055
+ if (!desktopHudSupported()) {
1056
+ return {
1057
+ changed: false,
1058
+ skipped: true,
1059
+ reason: "Desktop HUD is not supported on this platform"
1060
+ };
1061
+ }
1062
+
1063
+ const autostart = runHudCommand(["autostart", "off"]);
1064
+ const stop = runHudCommand(["stop"]);
1065
+ return {
1066
+ changed: autostart.ok || stop.ok,
1067
+ autostart,
1068
+ stop
1069
+ };
1070
+ }
1071
+
1072
+ function runTeardown(args) {
1073
+ const targets = teardownTargets(args.targets);
1074
+ const results = {};
1075
+
1076
+ const runStep = (name, action) => {
1077
+ results[name] = action();
1078
+ };
1079
+
1080
+ if (targets.includes("claude")) {
1081
+ runStep("claude", () => uninstallClaudeStatusline({ strict: false }));
1082
+ }
1083
+ if (targets.includes("codex")) {
1084
+ runStep("codex", uninstallCodexWrapper);
1085
+ }
1086
+ if (targets.includes("hud")) {
1087
+ runStep("hud", uninstallHud);
1088
+ }
1089
+ return {
1090
+ targets,
1091
+ results
1092
+ };
1093
+ }
1094
+
699
1095
  function safeStat(filePath) {
700
1096
  try {
701
1097
  return fs.statSync(filePath);
@@ -1546,13 +1942,37 @@ function readClaudeStatuslineCache() {
1546
1942
  return readClaudeStatuslineCacheFrom(legacyStateDir()) ?? null;
1547
1943
  }
1548
1944
 
1945
+ function claudeStatuslineRefreshInterval() {
1946
+ const value = Number(process.env.AI_BATTERY_CLAUDE_REFRESH ?? process.env.CLAUDEX_BATTERY_CLAUDE_REFRESH);
1947
+ if (!Number.isFinite(value)) return 1;
1948
+ return clamp(Math.floor(value), 1, 60);
1949
+ }
1950
+
1549
1951
  function installClaudeStatusline(args) {
1550
1952
  const settingsPath = homePath(".claude", "settings.json");
1551
1953
  const existing = readJson(settingsPath) ?? {};
1552
1954
  const command = `${shellArg(process.execPath)} ${shellArg(fileURLToPath(import.meta.url))} capture-claude --muted --left-padding 1`;
1955
+ const currentCommand = existing.statusLine?.command ?? "";
1956
+ const installedByAiBattery = currentCommand.includes("ai-battery.js") && currentCommand.includes("capture-claude");
1957
+ const config = readConfig();
1553
1958
 
1554
- if (existing.statusLine && !args.force) {
1555
- throw new Error(`Claude statusLine already exists in ${settingsPath}. Re-run with --force to replace it.`);
1959
+ if (existing.statusLine && !installedByAiBattery && !args.force) {
1960
+ return {
1961
+ settingsPath,
1962
+ command,
1963
+ skipped: true,
1964
+ reason: "Claude statusLine is already configured by another tool. Re-run with --force to replace it with a restorable backup."
1965
+ };
1966
+ }
1967
+
1968
+ let backedUp = false;
1969
+ if (existing.statusLine && !installedByAiBattery && args.force) {
1970
+ config.claudeStatusLineBackup = {
1971
+ settingsPath,
1972
+ statusLine: existing.statusLine,
1973
+ savedAt: new Date().toISOString()
1974
+ };
1975
+ backedUp = true;
1556
1976
  }
1557
1977
 
1558
1978
  const next = {
@@ -1561,22 +1981,27 @@ function installClaudeStatusline(args) {
1561
1981
  type: "command",
1562
1982
  command,
1563
1983
  padding: 0,
1564
- refreshInterval: 5
1984
+ refreshInterval: claudeStatuslineRefreshInterval()
1565
1985
  }
1566
1986
  };
1567
1987
 
1568
1988
  writeJsonAtomic(settingsPath, next);
1989
+ if (backedUp) writeConfig(config);
1569
1990
  return {
1570
1991
  settingsPath,
1571
- command
1992
+ command,
1993
+ backedUp
1572
1994
  };
1573
1995
  }
1574
1996
 
1575
- function uninstallClaudeStatusline() {
1997
+ function uninstallClaudeStatusline(options = {}) {
1576
1998
  const settingsPath = homePath(".claude", "settings.json");
1577
1999
  const existing = readJson(settingsPath) ?? {};
1578
2000
  const command = existing.statusLine?.command ?? "";
1579
2001
  const installedByAiBattery = command.includes("ai-battery.js") && command.includes("capture-claude");
2002
+ const strict = options.strict !== false;
2003
+ const config = readConfig();
2004
+ const backup = config.claudeStatusLineBackup;
1580
2005
 
1581
2006
  if (!existing.statusLine) {
1582
2007
  return {
@@ -1587,15 +2012,31 @@ function uninstallClaudeStatusline() {
1587
2012
  }
1588
2013
 
1589
2014
  if (!installedByAiBattery) {
2015
+ if (!strict) {
2016
+ return {
2017
+ settingsPath,
2018
+ changed: false,
2019
+ reason: "Claude statusLine is configured by another tool"
2020
+ };
2021
+ }
1590
2022
  throw new Error(`Claude statusLine exists but does not look like AI Battery's command: ${command}`);
1591
2023
  }
1592
2024
 
1593
2025
  const next = { ...existing };
1594
- delete next.statusLine;
2026
+ if (backup?.statusLine && (!backup.settingsPath || path.resolve(backup.settingsPath) === path.resolve(settingsPath))) {
2027
+ next.statusLine = backup.statusLine;
2028
+ } else {
2029
+ delete next.statusLine;
2030
+ }
1595
2031
  writeJsonAtomic(settingsPath, next);
2032
+ if (backup) {
2033
+ config.claudeStatusLineBackup = null;
2034
+ writeConfig(config);
2035
+ }
1596
2036
  return {
1597
2037
  settingsPath,
1598
- changed: true
2038
+ changed: true,
2039
+ restored: Boolean(backup?.statusLine)
1599
2040
  };
1600
2041
  }
1601
2042
 
@@ -1950,28 +2391,77 @@ function numericGuard(value) {
1950
2391
  return clamp(Math.floor(number), 0, 20);
1951
2392
  }
1952
2393
 
2394
+ function sttyColumns() {
2395
+ if (process.platform === "win32") return null;
2396
+
2397
+ let ttyFd = null;
2398
+ try {
2399
+ ttyFd = fs.openSync("/dev/tty", "r");
2400
+ const output = execFileSync("stty", ["size"], {
2401
+ encoding: "utf8",
2402
+ stdio: [ttyFd, "pipe", "ignore"],
2403
+ timeout: 100
2404
+ }).trim();
2405
+ const [, columns] = output.split(/\s+/).map((part) => Number(part));
2406
+ return numericColumn(columns);
2407
+ } catch {
2408
+ return null;
2409
+ } finally {
2410
+ if (ttyFd !== null) {
2411
+ try {
2412
+ fs.closeSync(ttyFd);
2413
+ } catch {
2414
+ // Nothing to clean up if the fd was already closed by the OS.
2415
+ }
2416
+ }
2417
+ }
2418
+ }
2419
+
2420
+ function runtimeTerminalColumns() {
2421
+ return numericColumn(process.stdout.columns) ?? sttyColumns();
2422
+ }
2423
+
1953
2424
  function statusLineColumns(input) {
1954
- const candidates = [
1955
- process.env.AI_BATTERY_COLUMNS,
1956
- process.env.CLAUDEX_BATTERY_COLUMNS,
2425
+ const explicitColumns = numericColumn(process.env.AI_BATTERY_COLUMNS)
2426
+ ?? numericColumn(process.env.CLAUDEX_BATTERY_COLUMNS);
2427
+ if (explicitColumns) return explicitColumns;
2428
+
2429
+ const runtimeColumns = runtimeTerminalColumns();
2430
+ const payloadCandidates = [
1957
2431
  input.terminal?.columns,
1958
2432
  input.terminal?.cols,
1959
2433
  input.terminal?.width,
1960
2434
  input.terminal_columns,
1961
2435
  input.terminal_width,
1962
2436
  input.columns,
1963
- input.width,
1964
- process.env.COLUMNS,
1965
- process.stdout.columns
2437
+ input.width
1966
2438
  ];
1967
2439
 
1968
- for (const candidate of candidates) {
2440
+ let payloadColumns = null;
2441
+ for (const candidate of payloadCandidates) {
1969
2442
  const columns = numericColumn(candidate);
1970
- if (columns) return columns;
2443
+ if (columns) {
2444
+ payloadColumns = columns;
2445
+ break;
2446
+ }
1971
2447
  }
2448
+
2449
+ if (payloadColumns && runtimeColumns) return Math.min(payloadColumns, runtimeColumns);
2450
+ if (payloadColumns) return payloadColumns;
2451
+ if (runtimeColumns) return runtimeColumns;
2452
+
2453
+ const envColumns = numericColumn(process.env.COLUMNS) ?? numericColumn(process.stdout.columns);
2454
+ if (envColumns) return envColumns;
1972
2455
  return 80;
1973
2456
  }
1974
2457
 
2458
+ function statusLineHeaderColumns(input, args) {
2459
+ const guard = numericGuard(process.env.AI_BATTERY_HEADER_COLUMN_GUARD)
2460
+ ?? numericGuard(process.env.CLAUDEX_BATTERY_HEADER_COLUMN_GUARD)
2461
+ ?? DEFAULT_STATUSLINE_HEADER_COLUMN_GUARD;
2462
+ return Math.max(20, statusLineColumns(input) - guard - (Number(args.leftPadding) || 0));
2463
+ }
2464
+
1975
2465
  function statusLineUsableColumns(input) {
1976
2466
  const guard = numericGuard(process.env.AI_BATTERY_COLUMN_GUARD)
1977
2467
  ?? numericGuard(process.env.CLAUDEX_BATTERY_COLUMN_GUARD)
@@ -2082,7 +2572,7 @@ function claudeHeader(input, args, capturedClaude = null) {
2082
2572
  }
2083
2573
  const left = parts.join(" ");
2084
2574
  const right = contextLeftText(input, capturedClaude?.contextWindow ?? null);
2085
- const columns = Math.max(20, statusLineUsableColumns(input) - (args.leftPadding || 0));
2575
+ const columns = statusLineHeaderColumns(input, args);
2086
2576
  const header = alignHeader(left, right, columns);
2087
2577
  return statusColorize({ running: false }, header, args);
2088
2578
  }
@@ -2252,6 +2742,71 @@ function render(snapshot, args) {
2252
2742
  return applyLeftPadding(renderLine(snapshot, args), args);
2253
2743
  }
2254
2744
 
2745
+ function printTeardownResult(result) {
2746
+ if (result.skipped) {
2747
+ console.log(`Teardown skipped: ${result.reason}`);
2748
+ return;
2749
+ }
2750
+
2751
+ const { codex, claude, hud } = result.results;
2752
+ if (codex) {
2753
+ if (codex.error) {
2754
+ console.log(`Codex wrapper error: ${codex.error}`);
2755
+ } else {
2756
+ for (const restored of codex.restoredWrappers || []) {
2757
+ console.log(`Restored Codex command: ${restored.wrapperPath}`);
2758
+ console.log(`Restored from: ${restored.restoredFrom}`);
2759
+ }
2760
+ for (const wrapperPath of codex.removedWrappers || []) {
2761
+ console.log(`Removed Codex wrapper: ${wrapperPath}`);
2762
+ }
2763
+ for (const skipped of codex.skippedWrappers || []) {
2764
+ console.log(`Left Codex wrapper untouched: ${skipped.wrapperPath}`);
2765
+ console.log(`Reason: ${skipped.reason}`);
2766
+ }
2767
+ if (!(codex.restoredWrappers?.length || codex.removedWrappers?.length || codex.skippedWrappers?.length)) {
2768
+ console.log(`Codex wrapper: ${codex.reason}`);
2769
+ }
2770
+ }
2771
+ for (const rcPath of codex.rcPaths || []) {
2772
+ console.log(`Removed shell PATH block: ${rcPath}`);
2773
+ }
2774
+ if (codex.configPath) console.log(`Updated ${codex.configPath}`);
2775
+ for (const wrapperPath of codex.unmanaged || []) {
2776
+ console.log(`Skipped unmanaged codex command: ${wrapperPath}`);
2777
+ }
2778
+ }
2779
+
2780
+ if (claude) {
2781
+ if (claude.error) {
2782
+ console.log(`Claude statusLine error: ${claude.error}`);
2783
+ } else if (claude.changed) {
2784
+ console.log(`${claude.restored ? "Restored previous Claude statusLine" : "Removed Claude statusLine"}: ${claude.settingsPath}`);
2785
+ } else {
2786
+ console.log(`${claude.reason}: ${claude.settingsPath}`);
2787
+ }
2788
+ }
2789
+
2790
+ if (hud) {
2791
+ if (hud.error) {
2792
+ console.log(`HUD error: ${hud.error}`);
2793
+ } else if (hud.skipped) {
2794
+ console.log(`HUD: ${hud.reason}`);
2795
+ } else {
2796
+ console.log(`HUD autostart: ${hud.autostart.ok ? "off" : "not changed"}`);
2797
+ if (!hud.autostart.ok && hud.autostart.error) console.log(`HUD autostart error: ${hud.autostart.error}`);
2798
+ if (!hud.autostart.ok && hud.autostart.stderr) console.log(`HUD autostart error: ${hud.autostart.stderr}`);
2799
+ console.log(`HUD process: ${hud.stop.ok ? "stopped" : "not changed"}`);
2800
+ if (!hud.stop.ok && hud.stop.error) console.log(`HUD stop error: ${hud.stop.error}`);
2801
+ if (!hud.stop.ok && hud.stop.stderr) console.log(`HUD stop error: ${hud.stop.stderr}`);
2802
+ }
2803
+ }
2804
+
2805
+ if (runningInsideAiBatteryCodexWrapper()) {
2806
+ console.log("Current Codex session was already started through AI Battery; exit this session to remove the terminal row.");
2807
+ }
2808
+ }
2809
+
2255
2810
  async function main() {
2256
2811
  const args = parseArgs(process.argv.slice(2));
2257
2812
  if (args.version) {
@@ -2269,7 +2824,12 @@ async function main() {
2269
2824
  console.log(JSON.stringify(result, null, 2));
2270
2825
  } else {
2271
2826
  if (result.claude) {
2272
- console.log(`Claude statusLine installed: ${result.claude.settingsPath}`);
2827
+ if (result.claude.skipped) {
2828
+ console.log(`Claude statusLine skipped: ${result.claude.reason}`);
2829
+ } else {
2830
+ console.log(`Claude statusLine installed: ${result.claude.settingsPath}`);
2831
+ if (result.claude.backedUp) console.log("Backed up previous Claude statusLine for uninstall restore.");
2832
+ }
2273
2833
  }
2274
2834
  if (result.codex?.ok) {
2275
2835
  console.log(`Codex wrapper installed: ${result.codex.wrapperPath}`);
@@ -2280,12 +2840,23 @@ async function main() {
2280
2840
  } else if (result.codex?.skipped) {
2281
2841
  console.log(`Codex wrapper skipped: ${result.codex.reason}`);
2282
2842
  }
2843
+ console.log("To remove AI Battery cleanly later, run: ai-battery uninstall");
2283
2844
  const note = codexRestartNote();
2284
2845
  if (result.codex && note) console.log(note);
2285
2846
  }
2286
2847
  return;
2287
2848
  }
2288
2849
 
2850
+ if (args.command === "teardown" || args.command === "uninstall") {
2851
+ const result = runTeardown(args);
2852
+ if (args.json) {
2853
+ console.log(JSON.stringify(result, null, 2));
2854
+ } else if (!args.silent) {
2855
+ printTeardownResult(result);
2856
+ }
2857
+ return;
2858
+ }
2859
+
2289
2860
  if (args.command === "doctor") {
2290
2861
  const result = await runDoctor();
2291
2862
  if (args.json) {
@@ -2351,8 +2922,13 @@ async function main() {
2351
2922
  if (args.command === "install-claude-statusline") {
2352
2923
  const result = installClaudeStatusline(args);
2353
2924
  if (!args.json) {
2354
- console.log(`Installed Claude statusLine: ${result.command}`);
2355
- console.log(`Updated ${result.settingsPath}`);
2925
+ if (result.skipped) {
2926
+ console.log(`Claude statusLine skipped: ${result.reason}`);
2927
+ } else {
2928
+ console.log(`Installed Claude statusLine: ${result.command}`);
2929
+ console.log(`Updated ${result.settingsPath}`);
2930
+ if (result.backedUp) console.log("Backed up previous Claude statusLine for uninstall restore.");
2931
+ }
2356
2932
  } else {
2357
2933
  console.log(JSON.stringify(result, null, 2));
2358
2934
  }
@@ -2363,7 +2939,7 @@ async function main() {
2363
2939
  const result = uninstallClaudeStatusline();
2364
2940
  if (!args.json) {
2365
2941
  if (result.changed) {
2366
- console.log(`Removed Claude statusLine from ${result.settingsPath}`);
2942
+ console.log(`${result.restored ? "Restored previous Claude statusLine in" : "Removed Claude statusLine from"} ${result.settingsPath}`);
2367
2943
  } else {
2368
2944
  console.log(`${result.reason}: ${result.settingsPath}`);
2369
2945
  }
@@ -2433,10 +3009,18 @@ async function main() {
2433
3009
  }
2434
3010
 
2435
3011
  export {
3012
+ claudeHeader,
3013
+ codexWrapperScript,
2436
3014
  firstPercentValue,
3015
+ installClaudeStatusline,
3016
+ installCodexWrapper,
2437
3017
  normalizeLimit,
3018
+ removeOrRestoreCodexWrapper,
3019
+ removeAiBatteryShellPathBlock,
2438
3020
  percentValue,
2439
- sameFilePath
3021
+ sameFilePath,
3022
+ visibleWidth,
3023
+ uninstallClaudeStatusline
2440
3024
  };
2441
3025
 
2442
3026
  function sameFilePath(leftPath, rightPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-battery",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Tiny terminal battery meter for local Codex and Claude Code usage.",
5
5
  "type": "module",
6
6
  "keywords": [