ai-battery 0.1.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/LICENSE +21 -0
- package/README.md +283 -0
- package/bin/ai-battery-hud +89 -0
- package/bin/ai-battery-hud.js +332 -0
- package/bin/ai-battery-hud.ps1 +1835 -0
- package/bin/ai-battery-run +344 -0
- package/bin/ai-battery.js +2082 -0
- package/docs/claude-statusline-preview.svg +37 -0
- package/docs/terminal-preview.svg +44 -0
- package/package.json +48 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const scriptPath = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
10
|
+
const scriptDir = path.dirname(scriptPath);
|
|
11
|
+
const ps1Path = path.join(scriptDir, "ai-battery-hud.ps1");
|
|
12
|
+
|
|
13
|
+
function isWsl() {
|
|
14
|
+
if (process.platform !== "linux") return false;
|
|
15
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
16
|
+
try {
|
|
17
|
+
return fs.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft");
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function commandExists(command) {
|
|
24
|
+
const result = spawnSync(command, ["-NoProfile", "-Command", "$PSVersionTable.PSVersion.Major"], {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: "ignore",
|
|
27
|
+
windowsHide: true
|
|
28
|
+
});
|
|
29
|
+
return result.status === 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function wslPath(filePath) {
|
|
33
|
+
const result = spawnSync("wslpath", ["-w", filePath], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
36
|
+
});
|
|
37
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
38
|
+
throw new Error("wslpath failed while resolving the HUD PowerShell script path");
|
|
39
|
+
}
|
|
40
|
+
return result.stdout.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function shellQuote(value) {
|
|
44
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function wslEnvPrefix() {
|
|
48
|
+
const names = [
|
|
49
|
+
"AI_BATTERY_STATE_DIR",
|
|
50
|
+
"CLAUDEX_BATTERY_STATE_DIR",
|
|
51
|
+
"AI_BATTERY_COLUMNS",
|
|
52
|
+
"CLAUDEX_BATTERY_COLUMNS",
|
|
53
|
+
"AI_BATTERY_COLUMN_GUARD",
|
|
54
|
+
"CLAUDEX_BATTERY_COLUMN_GUARD",
|
|
55
|
+
"CODEX_HOME"
|
|
56
|
+
];
|
|
57
|
+
return names
|
|
58
|
+
.filter((name) => process.env[name])
|
|
59
|
+
.map((name) => `${name}=${shellQuote(process.env[name])}`)
|
|
60
|
+
.join(" ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function psSingleQuote(value) {
|
|
64
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function winArgQuote(value) {
|
|
68
|
+
return `"${String(value).replace(/"/g, '\\"')}"`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildStartProcessCommand(filePath, args) {
|
|
72
|
+
const commandLine = args.map(winArgQuote).join(" ");
|
|
73
|
+
return `Start-Process -WindowStyle Hidden -FilePath ${psSingleQuote(filePath)} -ArgumentList ${psSingleQuote(commandLine)}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sleepSync(ms) {
|
|
77
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function snapshotHasWeekly(jsonText) {
|
|
81
|
+
try {
|
|
82
|
+
const snapshot = JSON.parse(jsonText);
|
|
83
|
+
if (!Array.isArray(snapshot.results)) return false;
|
|
84
|
+
const usageResults = snapshot.results.filter((result) => {
|
|
85
|
+
return result
|
|
86
|
+
&& result.ok
|
|
87
|
+
&& ["codex", "claude"].includes(result.provider)
|
|
88
|
+
&& typeof result.percentRemaining === "number";
|
|
89
|
+
});
|
|
90
|
+
return usageResults.length > 0 && usageResults.every((result) => result.secondary);
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function prefetchInitialJson(command, useWslCommand) {
|
|
97
|
+
if (process.env.AI_BATTERY_HUD_NO_PREFETCH) return null;
|
|
98
|
+
|
|
99
|
+
const attempts = 8;
|
|
100
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
101
|
+
const result = useWslCommand
|
|
102
|
+
? spawnSync("bash", ["-lc", command], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 })
|
|
103
|
+
: spawnSync(process.execPath, [batteryJs, "--json"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000 });
|
|
104
|
+
|
|
105
|
+
const text = result.stdout?.trim();
|
|
106
|
+
if (result.status === 0 && text) {
|
|
107
|
+
if (snapshotHasWeekly(text) || attempt === attempts - 1) return text;
|
|
108
|
+
}
|
|
109
|
+
sleepSync(180);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const useWsl = isWsl();
|
|
116
|
+
const powershell = "powershell.exe";
|
|
117
|
+
|
|
118
|
+
if (!useWsl && process.platform !== "win32") {
|
|
119
|
+
console.error("ai-battery-hud: the floating HUD needs Windows (native or WSL).");
|
|
120
|
+
console.error("On macOS/Linux terminals use: ai-battery --watch");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!commandExists(powershell)) {
|
|
125
|
+
console.error("ai-battery-hud: powershell.exe is required for the Windows HUD.");
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const filteredArgs = [];
|
|
130
|
+
let foreground = false;
|
|
131
|
+
let once = false;
|
|
132
|
+
let stop = false;
|
|
133
|
+
let subcommand = null;
|
|
134
|
+
let autostartAction = "status";
|
|
135
|
+
|
|
136
|
+
const cliArgs = process.argv.slice(2);
|
|
137
|
+
for (let i = 0; i < cliArgs.length; i += 1) {
|
|
138
|
+
const arg = cliArgs[i];
|
|
139
|
+
if (arg === "-Foreground" || arg === "--foreground") {
|
|
140
|
+
foreground = true;
|
|
141
|
+
} else if (arg === "-Movable" || arg === "--movable") {
|
|
142
|
+
filteredArgs.push("-Movable");
|
|
143
|
+
} else if (arg === "-Once" || arg === "--once") {
|
|
144
|
+
once = true;
|
|
145
|
+
filteredArgs.push("-Once");
|
|
146
|
+
} else if (arg === "-Stop" || arg === "--stop" || arg === "stop") {
|
|
147
|
+
stop = true;
|
|
148
|
+
filteredArgs.push("-StopExisting");
|
|
149
|
+
} else if (arg === "start") {
|
|
150
|
+
// Launching is the default action.
|
|
151
|
+
} else if (arg === "status") {
|
|
152
|
+
subcommand = "status";
|
|
153
|
+
} else if (arg === "autostart") {
|
|
154
|
+
subcommand = "autostart";
|
|
155
|
+
const next = cliArgs[i + 1];
|
|
156
|
+
if (next === "on" || next === "off" || next === "status") {
|
|
157
|
+
autostartAction = next;
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
filteredArgs.push(arg);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const hudScript = useWsl ? wslPath(ps1Path) : ps1Path;
|
|
166
|
+
const nodePath = process.execPath;
|
|
167
|
+
const batteryJs = path.join(scriptDir, "ai-battery.js");
|
|
168
|
+
const configuredCommand = process.env.AI_BATTERY_COMMAND || process.env.CLAUDEX_BATTERY_COMMAND;
|
|
169
|
+
const envPrefix = useWsl ? wslEnvPrefix() : "";
|
|
170
|
+
const batteryCommand = configuredCommand || (useWsl
|
|
171
|
+
? `${envPrefix ? `${envPrefix} ` : ""}HOME=${shellQuote(os.homedir())} ${shellQuote(nodePath)} ${shellQuote(batteryJs)} --json`
|
|
172
|
+
: `${winArgQuote(nodePath)} ${winArgQuote(batteryJs)} --json`);
|
|
173
|
+
|
|
174
|
+
const AUTOSTART_REG_PATH = "HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
175
|
+
const AUTOSTART_REG_NAME = "AiBatteryHud";
|
|
176
|
+
|
|
177
|
+
function runPowerShell(command) {
|
|
178
|
+
return spawnSync(powershell, ["-NoProfile", "-Command", command], {
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
windowsHide: true
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function hudProcessStatus() {
|
|
185
|
+
const query = "$hud = Get-CimInstance Win32_Process | Where-Object { "
|
|
186
|
+
+ "$_.ProcessId -ne $PID -and "
|
|
187
|
+
+ "$_.Name -match '^(powershell|pwsh)' -and "
|
|
188
|
+
+ "$_.CommandLine -like '*ai-battery-hud.ps1*' -and "
|
|
189
|
+
+ "$_.CommandLine -notlike '*Start-Process*' }; "
|
|
190
|
+
+ "if ($hud) { 'running (PID ' + @($hud)[0].ProcessId + ')' } else { 'stopped' }";
|
|
191
|
+
const result = runPowerShell(query);
|
|
192
|
+
return (result.stdout || "").trim() || "unknown";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function autostartStatus() {
|
|
196
|
+
const result = runPowerShell(
|
|
197
|
+
`$v = (Get-ItemProperty -Path '${AUTOSTART_REG_PATH}' -Name '${AUTOSTART_REG_NAME}' -ErrorAction SilentlyContinue).${AUTOSTART_REG_NAME}; `
|
|
198
|
+
+ "if ($v) { 'on'; $v } else { 'off' }"
|
|
199
|
+
);
|
|
200
|
+
const lines = (result.stdout || "").trim().split(/\r?\n/);
|
|
201
|
+
return { enabled: lines[0] === "on", command: lines.slice(1).join("\n") || null };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function autostartEnable() {
|
|
205
|
+
// autostart.ps1 refreshes the local copy of the HUD script when the source
|
|
206
|
+
// is reachable (the WSL share is not mounted until the distro starts), then
|
|
207
|
+
// launches the copy so sign-in start never depends on WSL being up. The HUD
|
|
208
|
+
// must run as a separate "powershell -File ...ai-battery-hud.ps1" process:
|
|
209
|
+
// stop/status and the single-instance cleanup all match that command line.
|
|
210
|
+
const hudArgLiterals = [
|
|
211
|
+
"'-NoProfile'",
|
|
212
|
+
"'-ExecutionPolicy'",
|
|
213
|
+
"'Bypass'",
|
|
214
|
+
"'-File'",
|
|
215
|
+
"('\"' + $hud + '\"')",
|
|
216
|
+
"'-BatteryCommand'",
|
|
217
|
+
"('\"' + ($battery -replace '\"', '\\\"') + '\"')"
|
|
218
|
+
];
|
|
219
|
+
if (useWsl) hudArgLiterals.push("'-UseWsl'");
|
|
220
|
+
|
|
221
|
+
const autostartScript = [
|
|
222
|
+
"# Generated by: ai-battery hud autostart on",
|
|
223
|
+
`$src = ${psSingleQuote(hudScript)}`,
|
|
224
|
+
"$hud = Join-Path $PSScriptRoot 'ai-battery-hud.ps1'",
|
|
225
|
+
"try { Copy-Item $src $hud -Force -ErrorAction Stop } catch { }",
|
|
226
|
+
`$battery = ${psSingleQuote(batteryCommand)}`,
|
|
227
|
+
`$argList = @(${hudArgLiterals.join(", ")}) -join ' '`,
|
|
228
|
+
"Start-Process -WindowStyle Hidden -FilePath 'powershell.exe' -ArgumentList $argList",
|
|
229
|
+
""
|
|
230
|
+
].join("\r\n");
|
|
231
|
+
const autostartB64 = Buffer.from(autostartScript, "utf8").toString("base64");
|
|
232
|
+
|
|
233
|
+
const script = [
|
|
234
|
+
"$dir = Join-Path $env:LOCALAPPDATA 'ai-battery'",
|
|
235
|
+
"New-Item -ItemType Directory -Force -Path $dir | Out-Null",
|
|
236
|
+
"$auto = Join-Path $dir 'autostart.ps1'",
|
|
237
|
+
`[System.IO.File]::WriteAllText($auto, [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${autostartB64}')))`,
|
|
238
|
+
`Copy-Item ${psSingleQuote(hudScript)} (Join-Path $dir 'ai-battery-hud.ps1') -Force`,
|
|
239
|
+
`Set-ItemProperty -Path '${AUTOSTART_REG_PATH}' -Name '${AUTOSTART_REG_NAME}' -Value ('powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $auto + '"')`,
|
|
240
|
+
"Write-Output $auto"
|
|
241
|
+
].join("; ");
|
|
242
|
+
return runPowerShell(script);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function autostartDisable() {
|
|
246
|
+
return runPowerShell(
|
|
247
|
+
`Remove-ItemProperty -Path '${AUTOSTART_REG_PATH}' -Name '${AUTOSTART_REG_NAME}' -ErrorAction SilentlyContinue`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (subcommand === "status") {
|
|
252
|
+
const auto = autostartStatus();
|
|
253
|
+
console.log(`HUD: ${hudProcessStatus()}`);
|
|
254
|
+
console.log(`Autostart: ${auto.enabled ? "on" : "off"}`);
|
|
255
|
+
if (auto.command) console.log(` ${auto.command}`);
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (subcommand === "autostart") {
|
|
260
|
+
if (autostartAction === "on") {
|
|
261
|
+
const result = autostartEnable();
|
|
262
|
+
const output = (result.stdout || "").trim();
|
|
263
|
+
if (result.status !== 0 || !output) {
|
|
264
|
+
console.error("ai-battery-hud: failed to register autostart.");
|
|
265
|
+
if ((result.stderr || "").trim()) console.error((result.stderr || "").trim());
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
console.log("HUD autostart enabled: launches at Windows sign-in.");
|
|
269
|
+
console.log(`Launcher: ${output}`);
|
|
270
|
+
console.log("After updating ai-battery, run \"ai-battery hud autostart on\" again to refresh it.");
|
|
271
|
+
} else if (autostartAction === "off") {
|
|
272
|
+
autostartDisable();
|
|
273
|
+
console.log("HUD autostart disabled.");
|
|
274
|
+
} else {
|
|
275
|
+
const auto = autostartStatus();
|
|
276
|
+
console.log(`Autostart: ${auto.enabled ? "on" : "off"}`);
|
|
277
|
+
if (auto.command) console.log(` ${auto.command}`);
|
|
278
|
+
}
|
|
279
|
+
process.exit(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const initialJson = stop ? null : prefetchInitialJson(batteryCommand, useWsl);
|
|
283
|
+
|
|
284
|
+
const psArgs = [
|
|
285
|
+
"-NoProfile",
|
|
286
|
+
"-ExecutionPolicy",
|
|
287
|
+
"Bypass",
|
|
288
|
+
"-File",
|
|
289
|
+
hudScript,
|
|
290
|
+
"-BatteryCommand",
|
|
291
|
+
batteryCommand,
|
|
292
|
+
...filteredArgs
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
if (initialJson) {
|
|
296
|
+
psArgs.push("-InitialJsonBase64", Buffer.from(initialJson, "utf8").toString("base64"));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (useWsl) {
|
|
300
|
+
psArgs.push("-UseWsl");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (foreground || once || stop) {
|
|
304
|
+
const result = spawnSync(powershell, psArgs, { stdio: "inherit", windowsHide: true });
|
|
305
|
+
if (stop && (result.status ?? 0) === 0) {
|
|
306
|
+
console.log("AI Battery HUD stopped.");
|
|
307
|
+
}
|
|
308
|
+
process.exit(result.status ?? 0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (useWsl) {
|
|
312
|
+
const start = spawnSync(powershell, [
|
|
313
|
+
"-NoProfile",
|
|
314
|
+
"-ExecutionPolicy",
|
|
315
|
+
"Bypass",
|
|
316
|
+
"-Command",
|
|
317
|
+
buildStartProcessCommand("powershell.exe", psArgs)
|
|
318
|
+
], {
|
|
319
|
+
stdio: "ignore",
|
|
320
|
+
windowsHide: true
|
|
321
|
+
});
|
|
322
|
+
if (start.status !== 0) process.exit(start.status ?? 1);
|
|
323
|
+
} else {
|
|
324
|
+
const child = spawn(powershell, psArgs, {
|
|
325
|
+
detached: true,
|
|
326
|
+
stdio: "ignore",
|
|
327
|
+
windowsHide: true
|
|
328
|
+
});
|
|
329
|
+
child.unref();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log("AI Battery HUD started or already running. Drag it to place it; right-click and choose Exit to close.");
|