ai-battery 0.1.4 → 0.1.6
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 +7 -5
- package/bin/ai-battery-hud +15 -83
- package/bin/ai-battery-hud.js +30 -6
- package/bin/ai-battery-hud.ps1 +20 -1
- package/bin/ai-battery-macos-status.applescript +65 -9
- package/bin/ai-battery-run-win.js +339 -0
- package/bin/ai-battery.js +644 -68
- package/package.json +7 -2
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const BATTERY_BIN = process.env.AI_BATTERY_BIN
|
|
10
|
+
|| process.env.CLAUDEX_BATTERY_BIN
|
|
11
|
+
|| path.join(SCRIPT_DIR, "ai-battery.js");
|
|
12
|
+
const ANSI_RE = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
13
|
+
const DEFAULT_COLUMN_GUARD = 4;
|
|
14
|
+
const DEFAULT_LEFT_PADDING = 2;
|
|
15
|
+
|
|
16
|
+
function usage() {
|
|
17
|
+
console.log(`Usage: ai-battery-run-win [--interval SECONDS] [--bar-width N] [--provider auto|all|codex|claude] [--left-padding N] -- COMMAND [ARGS...]
|
|
18
|
+
|
|
19
|
+
Runs COMMAND and keeps AI Battery on the terminal bottom row.
|
|
20
|
+
On Windows, node-pty enables ConPTY row reservation when available; otherwise AI Battery falls back to a same-console overlay row.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseArgs(argv) {
|
|
24
|
+
const args = {
|
|
25
|
+
interval: Number(process.env.AI_BATTERY_INTERVAL || process.env.CLAUDEX_BATTERY_INTERVAL || 10),
|
|
26
|
+
barWidth: process.env.AI_BATTERY_BAR_WIDTH || process.env.CLAUDEX_BATTERY_BAR_WIDTH || "10",
|
|
27
|
+
provider: process.env.AI_BATTERY_PROVIDER || process.env.CLAUDEX_BATTERY_PROVIDER || "auto",
|
|
28
|
+
leftPadding: Number(process.env.AI_BATTERY_LEFT_PADDING || process.env.CLAUDEX_BATTERY_LEFT_PADDING || DEFAULT_LEFT_PADDING),
|
|
29
|
+
command: []
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
const arg = argv[i];
|
|
34
|
+
if (arg === "-h" || arg === "--help") {
|
|
35
|
+
usage();
|
|
36
|
+
process.exit(0);
|
|
37
|
+
} else if (arg === "--interval") {
|
|
38
|
+
args.interval = Math.max(0.5, Number(argv[++i]) || args.interval);
|
|
39
|
+
} else if (arg === "--bar-width") {
|
|
40
|
+
args.barWidth = argv[++i] || args.barWidth;
|
|
41
|
+
} else if (arg === "--provider") {
|
|
42
|
+
args.provider = argv[++i] || args.provider;
|
|
43
|
+
if (!["auto", "all", "codex", "claude"].includes(args.provider)) {
|
|
44
|
+
throw new Error("--provider must be one of: auto, all, codex, claude");
|
|
45
|
+
}
|
|
46
|
+
} else if (arg === "--left-padding") {
|
|
47
|
+
args.leftPadding = Math.max(0, Math.min(20, Number(argv[++i]) || args.leftPadding));
|
|
48
|
+
} else if (arg === "--") {
|
|
49
|
+
args.command = argv.slice(i + 1);
|
|
50
|
+
break;
|
|
51
|
+
} else {
|
|
52
|
+
args.command = argv.slice(i);
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!args.command.length) {
|
|
58
|
+
usage();
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
if (!Number.isFinite(args.interval)) args.interval = 10;
|
|
62
|
+
return args;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function inferProvider(command) {
|
|
66
|
+
const joined = command.join(" ").toLowerCase();
|
|
67
|
+
const names = command.map((part) => path.basename(part).toLowerCase());
|
|
68
|
+
if (names.some((name) => name.includes("codex")) || joined.includes("@openai/codex")) return "codex";
|
|
69
|
+
if (names.some((name) => name.includes("claude")) || joined.includes("@anthropic-ai/claude-code")) return "claude";
|
|
70
|
+
return "all";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function termSize() {
|
|
74
|
+
return {
|
|
75
|
+
cols: Math.max(20, process.stdout.columns || 80),
|
|
76
|
+
rows: Math.max(1, process.stdout.rows || 24)
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function columnGuard() {
|
|
81
|
+
const raw = process.env.AI_BATTERY_COLUMN_GUARD || process.env.CLAUDEX_BATTERY_COLUMN_GUARD;
|
|
82
|
+
const value = Number(raw);
|
|
83
|
+
return Number.isFinite(value) ? Math.max(0, Math.min(20, Math.floor(value))) : DEFAULT_COLUMN_GUARD;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripAnsi(text) {
|
|
87
|
+
return String(text).replace(ANSI_RE, "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fitAnsi(text, width, pad = true) {
|
|
91
|
+
const plain = stripAnsi(text);
|
|
92
|
+
if (plain.length <= width) return text + (pad ? " ".repeat(width - plain.length) : "");
|
|
93
|
+
const suffix = pad ? " " : "";
|
|
94
|
+
return plain.slice(0, Math.max(0, width - suffix.length)) + suffix;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function batteryCommand(args, activeProvider, maxWidth) {
|
|
98
|
+
const command = [
|
|
99
|
+
process.execPath,
|
|
100
|
+
BATTERY_BIN,
|
|
101
|
+
"--muted",
|
|
102
|
+
"--bar-width",
|
|
103
|
+
String(args.barWidth),
|
|
104
|
+
"--max-width",
|
|
105
|
+
String(maxWidth),
|
|
106
|
+
"--left-padding",
|
|
107
|
+
String(args.leftPadding)
|
|
108
|
+
];
|
|
109
|
+
if (activeProvider === "codex" || activeProvider === "claude") {
|
|
110
|
+
command.push("--active-provider", activeProvider);
|
|
111
|
+
}
|
|
112
|
+
if (args.provider !== "all") {
|
|
113
|
+
command.push("--provider", args.provider);
|
|
114
|
+
}
|
|
115
|
+
return command;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
class StatusLine {
|
|
119
|
+
constructor(args, activeProvider) {
|
|
120
|
+
this.args = args;
|
|
121
|
+
this.activeProvider = activeProvider;
|
|
122
|
+
this.text = "AI Battery starting...";
|
|
123
|
+
this.nextFetch = 0;
|
|
124
|
+
this.lastLine = "";
|
|
125
|
+
this.lastDraw = 0;
|
|
126
|
+
this.resize();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
resize() {
|
|
130
|
+
const size = termSize();
|
|
131
|
+
this.cols = size.cols;
|
|
132
|
+
this.rows = size.rows;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
refresh(force = false) {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
if (!force && now < this.nextFetch) return;
|
|
138
|
+
this.nextFetch = now + (this.args.interval * 1000);
|
|
139
|
+
const maxWidth = Math.max(20, this.cols - columnGuard());
|
|
140
|
+
const command = batteryCommand(this.args, this.activeProvider, maxWidth);
|
|
141
|
+
const result = spawnSync(command[0], command.slice(1), {
|
|
142
|
+
encoding: "utf8",
|
|
143
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
144
|
+
timeout: 1500,
|
|
145
|
+
windowsHide: true
|
|
146
|
+
});
|
|
147
|
+
const text = result.stdout?.trim();
|
|
148
|
+
if (result.status === 0 && text) this.text = text;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
draw(force = false) {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (!force && now - this.lastDraw < 150) return;
|
|
154
|
+
this.refresh(force);
|
|
155
|
+
const width = Math.max(1, this.cols - 1);
|
|
156
|
+
const line = fitAnsi(this.text, width, true);
|
|
157
|
+
if (!force && line === this.lastLine) return;
|
|
158
|
+
this.lastLine = line;
|
|
159
|
+
this.lastDraw = now;
|
|
160
|
+
process.stdout.write(`\x1b7\x1b[0m\x1b[${this.rows};1H\r\x1b[1G${line}\x1b[K\x1b[0m\x1b8`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
clear() {
|
|
164
|
+
process.stdout.write(`\x1b7\x1b[0m\x1b[${this.rows};1H\r\x1b[1G\x1b[2K\x1b8`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function quoteCmdArg(value) {
|
|
169
|
+
return `"${String(value).replace(/"/g, '""')}"`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveWindowsCommandFile(commandPath) {
|
|
173
|
+
if (path.extname(commandPath)) return commandPath;
|
|
174
|
+
for (const suffix of [".cmd", ".exe", ".bat", ".ps1"]) {
|
|
175
|
+
const candidate = `${commandPath}${suffix}`;
|
|
176
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
177
|
+
}
|
|
178
|
+
return commandPath;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function windowsCommand(command) {
|
|
182
|
+
const exe = resolveWindowsCommandFile(command[0]);
|
|
183
|
+
const rest = command.slice(1);
|
|
184
|
+
if (/\.(cmd|bat)$/i.test(exe)) {
|
|
185
|
+
return {
|
|
186
|
+
file: "cmd.exe",
|
|
187
|
+
args: ["/d", "/s", "/c", [exe, ...rest].map(quoteCmdArg).join(" ")]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (/\.ps1$/i.test(exe)) {
|
|
191
|
+
return {
|
|
192
|
+
file: "powershell.exe",
|
|
193
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", exe, ...rest]
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (/\.(js|mjs|cjs)$/i.test(exe)) {
|
|
197
|
+
return {
|
|
198
|
+
file: process.execPath,
|
|
199
|
+
args: [exe, ...rest]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return { file: exe, args: rest };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function loadNodePty() {
|
|
206
|
+
try {
|
|
207
|
+
const mod = await import("node-pty");
|
|
208
|
+
return mod.default ?? mod;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function runConPty(args, activeProvider) {
|
|
215
|
+
const pty = await loadNodePty();
|
|
216
|
+
if (!pty || !process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
217
|
+
|
|
218
|
+
const status = new StatusLine(args, activeProvider);
|
|
219
|
+
const size = termSize();
|
|
220
|
+
const childRows = Math.max(1, size.rows - 1);
|
|
221
|
+
const command = windowsCommand(args.command);
|
|
222
|
+
const term = pty.spawn(command.file, command.args, {
|
|
223
|
+
name: "xterm-256color",
|
|
224
|
+
cols: size.cols,
|
|
225
|
+
rows: childRows,
|
|
226
|
+
cwd: process.cwd(),
|
|
227
|
+
env: process.env
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const oldRaw = process.stdin.isRaw;
|
|
231
|
+
process.stdin.setRawMode?.(true);
|
|
232
|
+
process.stdin.resume();
|
|
233
|
+
const onInput = (data) => term.write(data.toString());
|
|
234
|
+
process.stdin.on("data", onInput);
|
|
235
|
+
|
|
236
|
+
const timer = setInterval(() => status.draw(false), Math.max(500, args.interval * 1000));
|
|
237
|
+
const onResize = () => {
|
|
238
|
+
status.resize();
|
|
239
|
+
term.resize(status.cols, Math.max(1, status.rows - 1));
|
|
240
|
+
status.draw(true);
|
|
241
|
+
};
|
|
242
|
+
process.stdout.on("resize", onResize);
|
|
243
|
+
|
|
244
|
+
return await new Promise((resolve) => {
|
|
245
|
+
term.onData((data) => {
|
|
246
|
+
process.stdout.write(data);
|
|
247
|
+
status.draw(false);
|
|
248
|
+
});
|
|
249
|
+
term.onExit(({ exitCode }) => {
|
|
250
|
+
clearInterval(timer);
|
|
251
|
+
process.stdout.off("resize", onResize);
|
|
252
|
+
process.stdin.off("data", onInput);
|
|
253
|
+
process.stdin.setRawMode?.(oldRaw);
|
|
254
|
+
status.clear();
|
|
255
|
+
resolve(exitCode ?? 0);
|
|
256
|
+
});
|
|
257
|
+
status.draw(true);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function runOverlay(args, activeProvider) {
|
|
262
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
263
|
+
console.error("ai-battery-run-win: stdin/stdout is not a real terminal.");
|
|
264
|
+
return 2;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const status = new StatusLine(args, activeProvider);
|
|
268
|
+
const command = windowsCommand(args.command);
|
|
269
|
+
const child = spawn(command.file, command.args, {
|
|
270
|
+
stdio: "inherit",
|
|
271
|
+
cwd: process.cwd(),
|
|
272
|
+
env: process.env,
|
|
273
|
+
windowsHide: false
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const timer = setInterval(() => status.draw(true), Math.max(750, args.interval * 1000));
|
|
277
|
+
const onResize = () => {
|
|
278
|
+
status.resize();
|
|
279
|
+
status.draw(true);
|
|
280
|
+
};
|
|
281
|
+
process.stdout.on("resize", onResize);
|
|
282
|
+
status.draw(true);
|
|
283
|
+
|
|
284
|
+
return new Promise((resolve) => {
|
|
285
|
+
child.on("exit", (code, signal) => {
|
|
286
|
+
clearInterval(timer);
|
|
287
|
+
process.stdout.off("resize", onResize);
|
|
288
|
+
status.clear();
|
|
289
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
290
|
+
});
|
|
291
|
+
child.on("error", (error) => {
|
|
292
|
+
clearInterval(timer);
|
|
293
|
+
process.stdout.off("resize", onResize);
|
|
294
|
+
status.clear();
|
|
295
|
+
console.error(`ai-battery-run-win: ${error.message}`);
|
|
296
|
+
resolve(1);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function main() {
|
|
302
|
+
if (process.platform !== "win32") {
|
|
303
|
+
console.error("ai-battery-run-win is only for native Windows.");
|
|
304
|
+
process.exit(2);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const args = parseArgs(process.argv.slice(2));
|
|
308
|
+
const commandProvider = inferProvider(args.command);
|
|
309
|
+
if (args.provider === "auto") args.provider = commandProvider;
|
|
310
|
+
const activeProvider = ["codex", "claude"].includes(commandProvider) ? commandProvider : null;
|
|
311
|
+
|
|
312
|
+
const ptyExit = await runConPty(args, activeProvider);
|
|
313
|
+
const exitCode = ptyExit === null ? await runOverlay(args, activeProvider) : ptyExit;
|
|
314
|
+
process.exit(exitCode);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export {
|
|
318
|
+
resolveWindowsCommandFile,
|
|
319
|
+
windowsCommand
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
function sameFilePath(leftPath, rightPath) {
|
|
323
|
+
try {
|
|
324
|
+
return fs.realpathSync(leftPath) === fs.realpathSync(rightPath);
|
|
325
|
+
} catch {
|
|
326
|
+
return path.resolve(leftPath) === path.resolve(rightPath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function isDirectRun() {
|
|
331
|
+
return Boolean(process.argv[1]) && sameFilePath(fileURLToPath(import.meta.url), process.argv[1]);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (isDirectRun()) {
|
|
335
|
+
main().catch((error) => {
|
|
336
|
+
console.error(`ai-battery-run-win: ${error.message}`);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
});
|
|
339
|
+
}
|