ai-battery 0.1.1 → 0.1.2
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 +5 -0
- package/bin/ai-battery.js +206 -22
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -103,9 +103,11 @@ npx ai-battery
|
|
|
103
103
|
ai-battery
|
|
104
104
|
ai-battery --watch 10
|
|
105
105
|
ai-battery --json
|
|
106
|
+
ai-battery --version
|
|
106
107
|
ai-battery --provider codex
|
|
107
108
|
ai-battery --provider claude
|
|
108
109
|
ai-battery setup
|
|
110
|
+
ai-battery doctor
|
|
109
111
|
ai-battery hud
|
|
110
112
|
ai-battery off codex
|
|
111
113
|
ai-battery on codex
|
|
@@ -118,6 +120,9 @@ ai-battery on codex
|
|
|
118
120
|
| `--json` | HUD나 다른 도구에서 쓰기 좋은 JSON을 출력합니다. |
|
|
119
121
|
| `--bar-width N` | 터미널 배터리 바 길이를 조정합니다. |
|
|
120
122
|
| `--show-paths` | 로그 파일 경로와 데이터 관측 시각을 함께 표시합니다. |
|
|
123
|
+
| `-v`, `--version` | 설치된 `ai-battery` 버전을 출력합니다. |
|
|
124
|
+
|
|
125
|
+
`doctor`는 설치 상태와 함께 npm latest 버전을 확인합니다. 네트워크가 막혀 있으면 버전 확인만 건너뛰고 나머지 진단은 계속 표시합니다.
|
|
121
126
|
|
|
122
127
|
## Setup
|
|
123
128
|
|
package/bin/ai-battery.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import https from "node:https";
|
|
4
5
|
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
9
|
|
|
9
10
|
const DEFAULT_TAIL_BYTES = 4 * 1024 * 1024;
|
|
10
11
|
const DEFAULT_MAX_FILES = 40;
|
|
@@ -46,6 +47,7 @@ function parseArgs(argv) {
|
|
|
46
47
|
force: false,
|
|
47
48
|
header: true,
|
|
48
49
|
help: false,
|
|
50
|
+
version: false,
|
|
49
51
|
targets: [],
|
|
50
52
|
rest: []
|
|
51
53
|
};
|
|
@@ -99,6 +101,8 @@ function parseArgs(argv) {
|
|
|
99
101
|
} else if (arg === "--menu-bar") {
|
|
100
102
|
args.menuBar = true;
|
|
101
103
|
args.style = "plain";
|
|
104
|
+
} else if (arg === "--version" || arg === "-v") {
|
|
105
|
+
args.version = true;
|
|
102
106
|
} else if (arg === "--help" || arg === "-h") {
|
|
103
107
|
args.help = true;
|
|
104
108
|
} else {
|
|
@@ -147,6 +151,7 @@ Options:
|
|
|
147
151
|
--tmux Emit tmux status-line color markup
|
|
148
152
|
--force Replace an existing Claude statusLine
|
|
149
153
|
--no-color Disable ANSI colors
|
|
154
|
+
-v, --version Show ai-battery version
|
|
150
155
|
-h, --help Show this help
|
|
151
156
|
|
|
152
157
|
Compatibility:
|
|
@@ -279,6 +284,132 @@ function scriptDir() {
|
|
|
279
284
|
return path.dirname(fileURLToPath(import.meta.url));
|
|
280
285
|
}
|
|
281
286
|
|
|
287
|
+
function packageInfo() {
|
|
288
|
+
const pkg = readJson(path.join(scriptDir(), "..", "package.json")) ?? {};
|
|
289
|
+
return {
|
|
290
|
+
name: pkg.name || "ai-battery",
|
|
291
|
+
version: pkg.version || "0.0.0"
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function npmRegistryPackageUrl(name) {
|
|
296
|
+
const encoded = String(name)
|
|
297
|
+
.split("/")
|
|
298
|
+
.map((part) => encodeURIComponent(part))
|
|
299
|
+
.join("%2F");
|
|
300
|
+
return `https://registry.npmjs.org/${encoded}/latest`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function compareVersions(left, right) {
|
|
304
|
+
const parse = (value) => {
|
|
305
|
+
const [main, prerelease = ""] = String(value || "0.0.0").replace(/^v/, "").split("-", 2);
|
|
306
|
+
return {
|
|
307
|
+
parts: main.split(".").map((part) => Number.parseInt(part, 10) || 0),
|
|
308
|
+
prerelease
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
const a = parse(left);
|
|
312
|
+
const b = parse(right);
|
|
313
|
+
for (let i = 0; i < 3; i += 1) {
|
|
314
|
+
const diff = (a.parts[i] || 0) - (b.parts[i] || 0);
|
|
315
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
316
|
+
}
|
|
317
|
+
if (a.prerelease && !b.prerelease) return -1;
|
|
318
|
+
if (!a.prerelease && b.prerelease) return 1;
|
|
319
|
+
if (a.prerelease === b.prerelease) return 0;
|
|
320
|
+
return a.prerelease > b.prerelease ? 1 : -1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function fetchJson(url, timeoutMs = 1800, redirectsLeft = 2) {
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
const request = https.get(url, {
|
|
326
|
+
headers: {
|
|
327
|
+
Accept: "application/vnd.npm.install-v1+json, application/json",
|
|
328
|
+
"User-Agent": "ai-battery"
|
|
329
|
+
}
|
|
330
|
+
}, (response) => {
|
|
331
|
+
const location = response.headers.location;
|
|
332
|
+
if (
|
|
333
|
+
location
|
|
334
|
+
&& response.statusCode >= 300
|
|
335
|
+
&& response.statusCode < 400
|
|
336
|
+
&& redirectsLeft > 0
|
|
337
|
+
) {
|
|
338
|
+
response.resume();
|
|
339
|
+
const nextUrl = new URL(location, url).toString();
|
|
340
|
+
fetchJson(nextUrl, timeoutMs, redirectsLeft - 1).then(resolve);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let body = "";
|
|
345
|
+
response.setEncoding("utf8");
|
|
346
|
+
response.on("data", (chunk) => {
|
|
347
|
+
body += chunk;
|
|
348
|
+
if (body.length > 1024 * 1024) request.destroy(new Error("response too large"));
|
|
349
|
+
});
|
|
350
|
+
response.on("end", () => {
|
|
351
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
352
|
+
resolve({ ok: false, error: `HTTP ${response.statusCode}` });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
resolve({ ok: true, value: JSON.parse(body) });
|
|
357
|
+
} catch {
|
|
358
|
+
resolve({ ok: false, error: "invalid JSON from npm registry" });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
request.setTimeout(timeoutMs, () => {
|
|
364
|
+
request.destroy(new Error("timeout"));
|
|
365
|
+
});
|
|
366
|
+
request.on("error", (error) => {
|
|
367
|
+
resolve({ ok: false, error: error.message || String(error) });
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function checkPackageVersion() {
|
|
373
|
+
const current = packageInfo();
|
|
374
|
+
const checkedAt = new Date().toISOString();
|
|
375
|
+
if (process.env.AI_BATTERY_NO_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER) {
|
|
376
|
+
return {
|
|
377
|
+
name: current.name,
|
|
378
|
+
current: current.version,
|
|
379
|
+
latest: null,
|
|
380
|
+
updateAvailable: false,
|
|
381
|
+
checked: false,
|
|
382
|
+
checkedAt,
|
|
383
|
+
error: "disabled"
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const result = await fetchJson(npmRegistryPackageUrl(current.name));
|
|
388
|
+
if (!result.ok) {
|
|
389
|
+
return {
|
|
390
|
+
name: current.name,
|
|
391
|
+
current: current.version,
|
|
392
|
+
latest: null,
|
|
393
|
+
updateAvailable: false,
|
|
394
|
+
checked: false,
|
|
395
|
+
checkedAt,
|
|
396
|
+
error: result.error
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const latest = result.value?.version ?? null;
|
|
401
|
+
const comparison = latest ? compareVersions(latest, current.version) : 0;
|
|
402
|
+
return {
|
|
403
|
+
name: current.name,
|
|
404
|
+
current: current.version,
|
|
405
|
+
latest,
|
|
406
|
+
updateAvailable: comparison > 0,
|
|
407
|
+
checked: Boolean(latest),
|
|
408
|
+
checkedAt,
|
|
409
|
+
error: latest ? null : "npm registry response did not include a version"
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
282
413
|
function shQuote(value) {
|
|
283
414
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
284
415
|
}
|
|
@@ -518,11 +649,21 @@ function diagnoseCodex() {
|
|
|
518
649
|
};
|
|
519
650
|
}
|
|
520
651
|
|
|
521
|
-
function runDoctor() {
|
|
652
|
+
async function runDoctor() {
|
|
653
|
+
const version = await checkPackageVersion();
|
|
522
654
|
return {
|
|
523
655
|
generatedAt: new Date().toISOString(),
|
|
524
656
|
aiBattery: {
|
|
525
657
|
script: fileURLToPath(import.meta.url),
|
|
658
|
+
packageName: version.name,
|
|
659
|
+
version: version.current,
|
|
660
|
+
latestVersion: version.latest,
|
|
661
|
+
updateAvailable: version.updateAvailable,
|
|
662
|
+
updateCheck: {
|
|
663
|
+
checked: version.checked,
|
|
664
|
+
checkedAt: version.checkedAt,
|
|
665
|
+
error: version.error
|
|
666
|
+
},
|
|
526
667
|
stateDir: stateDir(),
|
|
527
668
|
configPath: configPath()
|
|
528
669
|
},
|
|
@@ -687,21 +828,37 @@ function prioritizeCodexSessionFiles(files) {
|
|
|
687
828
|
});
|
|
688
829
|
}
|
|
689
830
|
|
|
690
|
-
function
|
|
831
|
+
function firstFiniteEntry(source, keys) {
|
|
691
832
|
for (const key of [keys].flat().filter(Boolean)) {
|
|
692
833
|
const value = source?.[key];
|
|
693
|
-
if (Number.isFinite(value)) return value;
|
|
834
|
+
if (Number.isFinite(value)) return { key, value };
|
|
694
835
|
if (typeof value === "string" && value.trim()) {
|
|
695
836
|
const numeric = Number(value);
|
|
696
|
-
if (Number.isFinite(numeric)) return numeric;
|
|
837
|
+
if (Number.isFinite(numeric)) return { key, value: numeric };
|
|
697
838
|
}
|
|
698
839
|
}
|
|
699
840
|
return null;
|
|
700
841
|
}
|
|
701
842
|
|
|
702
|
-
function
|
|
843
|
+
function firstFiniteValue(source, keys) {
|
|
844
|
+
return firstFiniteEntry(source, keys)?.value ?? null;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function keyUsesFractionalPercent(key) {
|
|
848
|
+
return /(?:ratio|fraction|utilization)$/i.test(String(key));
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function percentValue(value, options = {}) {
|
|
703
852
|
if (!Number.isFinite(value)) return null;
|
|
704
|
-
return value >= 0 && value <= 1 ? value * 100 : value;
|
|
853
|
+
return options.scaleFraction && value >= 0 && value <= 1 ? value * 100 : value;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function firstPercentValue(source, keys) {
|
|
857
|
+
const entry = firstFiniteEntry(source, keys);
|
|
858
|
+
if (!entry) return null;
|
|
859
|
+
return percentValue(entry.value, {
|
|
860
|
+
scaleFraction: keyUsesFractionalPercent(entry.key)
|
|
861
|
+
});
|
|
705
862
|
}
|
|
706
863
|
|
|
707
864
|
function usageInputTokens(usage) {
|
|
@@ -736,8 +893,8 @@ function normalizeLimit(limit, options = {}) {
|
|
|
736
893
|
const usedKeys = options.usedKey || "used_percent";
|
|
737
894
|
const remainingKeys = [options.remainingKey].flat().filter(Boolean);
|
|
738
895
|
const windowMinutes = options.windowMinutes ?? limit?.window_minutes ?? limit?.windowMinutes ?? null;
|
|
739
|
-
const usedValue =
|
|
740
|
-
const remainingValue =
|
|
896
|
+
const usedValue = firstPercentValue(limit, usedKeys);
|
|
897
|
+
const remainingValue = firstPercentValue(limit, remainingKeys);
|
|
741
898
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
742
899
|
const resetsAtSeconds = resetEpochSeconds(limit.resets_at ?? limit.resetsAt ?? limit.reset_at ?? limit.resetAt);
|
|
743
900
|
const resetPassed = Number.isFinite(resetsAtSeconds) && resetsAtSeconds <= nowSeconds;
|
|
@@ -788,7 +945,7 @@ function readJson(filePath) {
|
|
|
788
945
|
}
|
|
789
946
|
}
|
|
790
947
|
|
|
791
|
-
const SCAN_CACHE_VERSION =
|
|
948
|
+
const SCAN_CACHE_VERSION = 2;
|
|
792
949
|
|
|
793
950
|
function scanCacheSeconds(defaultSeconds) {
|
|
794
951
|
const raw = Number(process.env.AI_BATTERY_SCAN_CACHE_SECONDS);
|
|
@@ -999,20 +1156,20 @@ function readStdin() {
|
|
|
999
1156
|
|
|
1000
1157
|
function claudeLimitFromStatusline(limit, windowMinutes) {
|
|
1001
1158
|
if (!limit) return null;
|
|
1002
|
-
const usedPercentage =
|
|
1159
|
+
const usedPercentage = firstPercentValue(limit, [
|
|
1003
1160
|
"used_percentage",
|
|
1004
1161
|
"usedPercent",
|
|
1005
1162
|
"percent_used",
|
|
1006
1163
|
"percentUsed",
|
|
1007
1164
|
"utilization"
|
|
1008
|
-
])
|
|
1009
|
-
const remainingPercentage =
|
|
1165
|
+
]);
|
|
1166
|
+
const remainingPercentage = firstPercentValue(limit, [
|
|
1010
1167
|
"remaining_percentage",
|
|
1011
1168
|
"remainingPercent",
|
|
1012
1169
|
"remaining_percent",
|
|
1013
1170
|
"percent_remaining",
|
|
1014
1171
|
"percentRemaining"
|
|
1015
|
-
])
|
|
1172
|
+
]);
|
|
1016
1173
|
const resetsAt = resetEpochSeconds(limit.resets_at ?? limit.resetsAt ?? limit.reset_at ?? limit.resetAt);
|
|
1017
1174
|
const hasUsedPercentage = Number.isFinite(usedPercentage);
|
|
1018
1175
|
const hasRemainingPercentage = Number.isFinite(remainingPercentage);
|
|
@@ -1065,13 +1222,13 @@ function claudeRateLimitsFromStatusline(input) {
|
|
|
1065
1222
|
function normalizeClaudeContextWindow(context) {
|
|
1066
1223
|
if (!context) return null;
|
|
1067
1224
|
|
|
1068
|
-
const usedPercentage =
|
|
1069
|
-
let remainingPercentage =
|
|
1225
|
+
const usedPercentage = firstPercentValue(context, ["used_percentage", "usedPercentage", "percent_used", "percentUsed"]);
|
|
1226
|
+
let remainingPercentage = firstPercentValue(context, [
|
|
1070
1227
|
"remaining_percentage",
|
|
1071
1228
|
"remainingPercentage",
|
|
1072
1229
|
"percent_remaining",
|
|
1073
1230
|
"percentRemaining"
|
|
1074
|
-
])
|
|
1231
|
+
]);
|
|
1075
1232
|
const currentUsage = context.current_usage ?? context.currentUsage ?? null;
|
|
1076
1233
|
const contextWindowSize = firstFiniteValue(context, ["context_window_size", "contextWindowSize", "size", "max_tokens", "maxTokens"]);
|
|
1077
1234
|
const totalInputTokens = firstFiniteValue(context, ["total_input_tokens", "totalInputTokens"])
|
|
@@ -2097,6 +2254,10 @@ function render(snapshot, args) {
|
|
|
2097
2254
|
|
|
2098
2255
|
async function main() {
|
|
2099
2256
|
const args = parseArgs(process.argv.slice(2));
|
|
2257
|
+
if (args.version) {
|
|
2258
|
+
console.log(packageInfo().version);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2100
2261
|
if (args.help) {
|
|
2101
2262
|
printHelp();
|
|
2102
2263
|
return;
|
|
@@ -2126,11 +2287,26 @@ async function main() {
|
|
|
2126
2287
|
}
|
|
2127
2288
|
|
|
2128
2289
|
if (args.command === "doctor") {
|
|
2129
|
-
const result = runDoctor();
|
|
2290
|
+
const result = await runDoctor();
|
|
2130
2291
|
if (args.json) {
|
|
2131
2292
|
console.log(JSON.stringify(result, null, 2));
|
|
2132
2293
|
} else {
|
|
2133
2294
|
console.log(`AI Battery: ${result.aiBattery.script}`);
|
|
2295
|
+
console.log(`Version: ${result.aiBattery.version}`);
|
|
2296
|
+
if (result.aiBattery.latestVersion) {
|
|
2297
|
+
console.log(`npm latest: ${result.aiBattery.latestVersion}`);
|
|
2298
|
+
if (result.aiBattery.updateAvailable) {
|
|
2299
|
+
console.log(`Update: available (npm install -g ${result.aiBattery.packageName}@latest)`);
|
|
2300
|
+
} else if (compareVersions(result.aiBattery.version, result.aiBattery.latestVersion) > 0) {
|
|
2301
|
+
console.log("Update: local version is newer than npm latest");
|
|
2302
|
+
} else {
|
|
2303
|
+
console.log("Update: up to date");
|
|
2304
|
+
}
|
|
2305
|
+
} else if (result.aiBattery.updateCheck.error === "disabled") {
|
|
2306
|
+
console.log("npm latest: skipped");
|
|
2307
|
+
} else {
|
|
2308
|
+
console.log(`npm latest: unavailable (${result.aiBattery.updateCheck.error || "unknown error"})`);
|
|
2309
|
+
}
|
|
2134
2310
|
console.log(`State: ${result.aiBattery.stateDir}`);
|
|
2135
2311
|
console.log("");
|
|
2136
2312
|
console.log(`Codex provider: ${result.codex.providerEnabled ? "on" : "off"}`);
|
|
@@ -2256,7 +2432,15 @@ async function main() {
|
|
|
2256
2432
|
}
|
|
2257
2433
|
}
|
|
2258
2434
|
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2435
|
+
export {
|
|
2436
|
+
firstPercentValue,
|
|
2437
|
+
normalizeLimit,
|
|
2438
|
+
percentValue
|
|
2439
|
+
};
|
|
2440
|
+
|
|
2441
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2442
|
+
main().catch((error) => {
|
|
2443
|
+
console.error(`ai-battery: ${error.message}`);
|
|
2444
|
+
process.exit(1);
|
|
2445
|
+
});
|
|
2446
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-battery",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Tiny terminal battery meter for local Codex and Claude Code usage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
],
|
|
33
33
|
"scripts": {
|
|
34
34
|
"start": "node ./bin/ai-battery.js",
|
|
35
|
-
"check": "node --check ./bin/ai-battery.js && node --check ./bin/ai-battery-hud.js"
|
|
35
|
+
"check": "node --check ./bin/ai-battery.js && node --check ./bin/ai-battery-hud.js && node --test",
|
|
36
|
+
"test": "node --test"
|
|
36
37
|
},
|
|
37
38
|
"engines": {
|
|
38
39
|
"node": ">=18"
|