@kairyou/agent-tools 0.7.4 → 0.9.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/README.md +31 -15
- package/README.zh-CN.md +28 -13
- package/dist/usage/core.mjs +123 -46
- package/dist/usage/opencode-plugin.mjs +1 -4
- package/dist/usage/opencode-tui.mjs +1 -4
- package/integrations/usage/lib/format.mjs +101 -29
- package/integrations/usage/lib/routes.mjs +45 -18
- package/integrations/usage/opencode-plugin.mjs +1 -5
- package/integrations/usage/opencode-tui.mjs +1 -5
- package/package.json +3 -2
- package/scripts/publish.mjs +108 -0
- package/scripts/release.mjs +122 -29
package/README.md
CHANGED
|
@@ -26,7 +26,9 @@ Generate a Conventional Commits message from staged changes and wait for user co
|
|
|
26
26
|
npx -y skills@latest add kairyou/agent-tools --skill at-commit -g -y
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Usage:
|
|
30
|
+
|
|
31
|
+
- `/at-commit [<language>]` — language for the commit description (Conventional Commits tokens stay in English)
|
|
30
32
|
|
|
31
33
|
### at-review
|
|
32
34
|
|
|
@@ -36,7 +38,9 @@ Review changes for correctness bugs, regressions, convention violations, and hig
|
|
|
36
38
|
npx -y skills@latest add kairyou/agent-tools --skill at-review -g -y
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Usage:
|
|
42
|
+
|
|
43
|
+
- `/at-review [--fix] [<pr|branch|path>]` — reports findings; `--fix` also applies them
|
|
40
44
|
|
|
41
45
|
### at-simplify
|
|
42
46
|
|
|
@@ -46,7 +50,9 @@ Refactor changes to reduce duplication, lower complexity, and improve code quali
|
|
|
46
50
|
npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
Usage:
|
|
54
|
+
|
|
55
|
+
- `/at-simplify [<pr|branch|path>]`
|
|
50
56
|
|
|
51
57
|
### at-zentao
|
|
52
58
|
|
|
@@ -97,7 +103,7 @@ The installer writes `statusLine` to `~/.claude/settings.json`. Example output:
|
|
|
97
103
|
⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
|
|
98
104
|
|
|
99
105
|
# With a compatible API relay, its quota info is shown instead, e.g.:
|
|
100
|
-
⎇ main | Opus 4.8 |
|
|
106
|
+
⎇ main | Opus 4.8 | balance $362 | today $61.7 | 30d $566
|
|
101
107
|
```
|
|
102
108
|
|
|
103
109
|
Here `5h` and `w` are Claude's rolling usage windows and `⟳` is the reset
|
|
@@ -109,9 +115,10 @@ compatibility and configuration.
|
|
|
109
115
|
For API relay / gateway setups: shows the relay's balance / quota inside the
|
|
110
116
|
agent, so when you pay per use or have plan limits you always know how much you
|
|
111
117
|
have spent and how much is left — without opening the gateway console.
|
|
112
|
-
Works with API-key usage endpoints exposed by
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
Works with API-key usage endpoints exposed by relay software such as Sub2API,
|
|
119
|
+
One API (including OneHub and DoneHub), New API, and Claude Code Hub. It also
|
|
120
|
+
supports the OpenRouter platform. Compatibility depends on the gateway version
|
|
121
|
+
and whether the corresponding usage endpoint is enabled.
|
|
115
122
|
|
|
116
123
|
```bash
|
|
117
124
|
npx -y @kairyou/agent-tools@latest usage -a claude codex opencode
|
|
@@ -138,7 +145,9 @@ endpoint and key — and tune `providerUsage` in `~/.agent-tools/config.jsonc`:
|
|
|
138
145
|
```jsonc
|
|
139
146
|
{
|
|
140
147
|
"providerUsage": {
|
|
141
|
-
|
|
148
|
+
// auto | sub2api | openai-compatible | one-api | one-hub |
|
|
149
|
+
// done-hub | new-api | claude-code-hub | openrouter | <custom-route-id>
|
|
150
|
+
"preset": "auto",
|
|
142
151
|
"days": 30, // how many recent days of spend to count
|
|
143
152
|
"debug": false // true: log probes to ~/.agent-tools/logs/usage-debug.log
|
|
144
153
|
}
|
|
@@ -152,15 +161,22 @@ custom route id is also accepted.
|
|
|
152
161
|
Output examples:
|
|
153
162
|
|
|
154
163
|
```text
|
|
155
|
-
#
|
|
156
|
-
|
|
164
|
+
# Plan limits (sub2api / openai-compatible).
|
|
165
|
+
D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
166
|
+
|
|
167
|
+
# Multi-window limits (claude-code-hub).
|
|
168
|
+
5h $2.1/$10.0 | D $8.0/$20.0 | T $19.0/$100 | Exp 08-31
|
|
169
|
+
|
|
170
|
+
# Balance and usage (one-api / one-hub / done-hub / new-api / openrouter).
|
|
171
|
+
balance $15.0 | used $5.0/$20.0
|
|
157
172
|
|
|
158
|
-
# Wallet
|
|
159
|
-
|
|
173
|
+
# Wallet and recent spend (sub2api).
|
|
174
|
+
balance $362 | today $61.7 | 30d $566
|
|
160
175
|
```
|
|
161
176
|
|
|
162
|
-
Fields: `D/W/M` are daily/weekly/monthly spend against
|
|
163
|
-
the plan expiry; `balance` is wallet credit; `today` and `30d`
|
|
177
|
+
Fields: `5h/D/W/M/T` are five-hour/daily/weekly/monthly/total spend against
|
|
178
|
+
limits; `Exp` is the plan expiry; `balance` is wallet credit; `today` and `30d`
|
|
179
|
+
are API spend.
|
|
164
180
|
|
|
165
181
|
#### Custom gateway routes
|
|
166
182
|
|
|
@@ -203,7 +219,7 @@ export async function run(context, { requestJson, agentConfig }) {
|
|
|
203
219
|
const me = await requestJson(`${context.baseUrl}/api/user/self`, {
|
|
204
220
|
headers: { authorization: `Bearer ${session?.data?.accessToken}` },
|
|
205
221
|
});
|
|
206
|
-
return { text: `
|
|
222
|
+
return { text: `balance ¥${me?.data?.balance}` };
|
|
207
223
|
}
|
|
208
224
|
```
|
|
209
225
|
|
package/README.zh-CN.md
CHANGED
|
@@ -24,7 +24,9 @@ npx -y skills@latest add kairyou/agent-tools --skill <name...> -g -y
|
|
|
24
24
|
npx -y skills@latest add kairyou/agent-tools --skill at-commit -g -y
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
用法:
|
|
28
|
+
|
|
29
|
+
- `/at-commit [<语言>]` — 指定 commit description 的语言(Conventional Commits 语法部分保持英文)
|
|
28
30
|
|
|
29
31
|
### at-review
|
|
30
32
|
|
|
@@ -34,7 +36,9 @@ npx -y skills@latest add kairyou/agent-tools --skill at-commit -g -y
|
|
|
34
36
|
npx -y skills@latest add kairyou/agent-tools --skill at-review -g -y
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
用法:
|
|
40
|
+
|
|
41
|
+
- `/at-review [--fix] [<pr|分支|路径>]` — 输出审查结果; 加 `--fix` 则同时应用修复
|
|
38
42
|
|
|
39
43
|
### at-simplify
|
|
40
44
|
|
|
@@ -44,7 +48,9 @@ npx -y skills@latest add kairyou/agent-tools --skill at-review -g -y
|
|
|
44
48
|
npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
用法:
|
|
52
|
+
|
|
53
|
+
- `/at-simplify [<pr|分支|路径>]`
|
|
48
54
|
|
|
49
55
|
### at-zentao
|
|
50
56
|
|
|
@@ -94,7 +100,7 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
|
94
100
|
⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
|
|
95
101
|
|
|
96
102
|
# 使用兼容的 API 中转时, 显示中转的额度信息, 比如:
|
|
97
|
-
⎇ main | Opus 4.8 |
|
|
103
|
+
⎇ main | Opus 4.8 | balance $362 | today $61.7 | 30d $566
|
|
98
104
|
```
|
|
99
105
|
|
|
100
106
|
其中 `5h` / `w` 是 Claude 的滚动用量窗口, `⟳` 后面是重置倒计时;
|
|
@@ -104,8 +110,9 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
|
104
110
|
|
|
105
111
|
面向使用 API 中转的场景: 在 agent 内直接显示中转网关的余额/额度, 按量付费或
|
|
106
112
|
有套餐限额时, 随时知道花了多少, 还剩多少, 不用切出去登录网关后台.
|
|
107
|
-
支持 Sub2API, One API, New API 与
|
|
108
|
-
|
|
113
|
+
支持 Sub2API, One API (包括 OneHub 与 DoneHub), New API 与 Claude Code Hub
|
|
114
|
+
等中转程序提供的 API Key 用量接口, 也支持 OpenRouter 平台. 具体兼容性取决于
|
|
115
|
+
网关版本及其是否开放相应接口.
|
|
109
116
|
|
|
110
117
|
```bash
|
|
111
118
|
npx -y @kairyou/agent-tools@latest usage -a claude codex opencode
|
|
@@ -130,7 +137,9 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
130
137
|
```jsonc
|
|
131
138
|
{
|
|
132
139
|
"providerUsage": {
|
|
133
|
-
|
|
140
|
+
// auto | sub2api | openai-compatible | one-api | one-hub |
|
|
141
|
+
// done-hub | new-api | claude-code-hub | openrouter | <自定义 route id>
|
|
142
|
+
"preset": "auto",
|
|
134
143
|
"days": 30, // 统计最近多少天的消耗
|
|
135
144
|
"debug": false // true: 探测过程写入 ~/.agent-tools/logs/usage-debug.log
|
|
136
145
|
}
|
|
@@ -143,14 +152,20 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
143
152
|
显示效果示例:
|
|
144
153
|
|
|
145
154
|
```text
|
|
146
|
-
#
|
|
147
|
-
|
|
155
|
+
# 套餐限额 (sub2api / openai-compatible).
|
|
156
|
+
D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
157
|
+
|
|
158
|
+
# 多窗口限额 (claude-code-hub).
|
|
159
|
+
5h $2.1/$10.0 | D $8.0/$20.0 | T $19.0/$100 | Exp 08-31
|
|
160
|
+
|
|
161
|
+
# 余额与用量 (one-api / one-hub / done-hub / new-api / openrouter).
|
|
162
|
+
balance $15.0 | used $5.0/$20.0
|
|
148
163
|
|
|
149
|
-
#
|
|
150
|
-
|
|
164
|
+
# 钱包与近期消耗 (sub2api).
|
|
165
|
+
balance $362 | today $61.7 | 30d $566
|
|
151
166
|
```
|
|
152
167
|
|
|
153
|
-
字段含义: `D/W/M`
|
|
168
|
+
字段含义: `5h/D/W/M/T` 是 5 小时/日/周/月/总消耗与上限, `Exp` 是套餐到期日,
|
|
154
169
|
`balance` 是钱包余额, `today` / `30d` 是今日与近 30 天 API 消耗.
|
|
155
170
|
|
|
156
171
|
#### 自定义网关路由
|
|
@@ -192,7 +207,7 @@ export async function run(context, { requestJson, agentConfig }) {
|
|
|
192
207
|
const me = await requestJson(`${context.baseUrl}/api/user/self`, {
|
|
193
208
|
headers: { authorization: `Bearer ${session?.data?.accessToken}` },
|
|
194
209
|
});
|
|
195
|
-
return { text: `
|
|
210
|
+
return { text: `balance ¥${me?.data?.balance}` };
|
|
196
211
|
}
|
|
197
212
|
```
|
|
198
213
|
|
package/dist/usage/core.mjs
CHANGED
|
@@ -1261,6 +1261,7 @@ async function requestJson(url, options = {}) {
|
|
|
1261
1261
|
}
|
|
1262
1262
|
|
|
1263
1263
|
// integrations/usage/lib/format.mjs
|
|
1264
|
+
var ONE_API_HARD_LIMIT_SENTINEL_USD = 1e6;
|
|
1264
1265
|
function pickNumber(obj, keys) {
|
|
1265
1266
|
for (const key of keys) {
|
|
1266
1267
|
const value = obj?.[key];
|
|
@@ -1276,9 +1277,6 @@ function formatMaybeMoney(value, unit = "USD") {
|
|
|
1276
1277
|
if (unit === "USD" || unit === "$") return formatMoney(value);
|
|
1277
1278
|
return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
|
|
1278
1279
|
}
|
|
1279
|
-
function usageParts() {
|
|
1280
|
-
return ["API"];
|
|
1281
|
-
}
|
|
1282
1280
|
async function formatNewApiQuota(value) {
|
|
1283
1281
|
const scale = await newApiQuotaScale();
|
|
1284
1282
|
if (Number.isFinite(scale) && scale > 0) return formatMoney(value / scale);
|
|
@@ -1328,40 +1326,58 @@ function hasV1UsageFields(root) {
|
|
|
1328
1326
|
"usage"
|
|
1329
1327
|
]) !== void 0 || pickNumber(root?.quota, ["limit", "quota", "used", "quota_used", "remaining"]) !== void 0 || pickNumber(root?.usage?.today, ["actual_cost", "cost"]) !== void 0 || Array.isArray(root?.daily_usage) && root.daily_usage.length > 0;
|
|
1330
1328
|
}
|
|
1331
|
-
async function formatQuota(
|
|
1329
|
+
async function formatQuota(data) {
|
|
1332
1330
|
const root = usageRoot(data);
|
|
1333
1331
|
const unit = root?.unit || "USD";
|
|
1334
1332
|
const remaining = pickNumber(root, ["remaining"]);
|
|
1335
1333
|
const hardLimit = pickNumber(root, ["hard_limit_usd", "hard_limit", "total_granted", "quota"]);
|
|
1336
1334
|
const used = pickNumber(root, ["total_usage", "used", "usage"]);
|
|
1337
1335
|
const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
|
|
1338
|
-
if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(
|
|
1339
|
-
if (isSubscriptionUsage(root)) return formatUsageLine(
|
|
1340
|
-
if (isWalletUsage(root)) return await formatWalletLine(
|
|
1341
|
-
if (remaining !== void 0) return formatUsageLine(
|
|
1342
|
-
if (balance !== void 0) return `
|
|
1336
|
+
if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(root);
|
|
1337
|
+
if (isSubscriptionUsage(root)) return formatUsageLine(root);
|
|
1338
|
+
if (isWalletUsage(root)) return await formatWalletLine(root);
|
|
1339
|
+
if (remaining !== void 0) return formatUsageLine(root);
|
|
1340
|
+
if (balance !== void 0) return `balance ${formatMaybeMoney(balance, unit)}`;
|
|
1343
1341
|
if (hardLimit !== void 0 && used !== void 0) {
|
|
1344
|
-
return `
|
|
1342
|
+
return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
|
|
1345
1343
|
}
|
|
1346
|
-
if (hardLimit !== void 0) return `
|
|
1344
|
+
if (hardLimit !== void 0) return `total ${formatMaybeMoney(hardLimit, unit)}`;
|
|
1347
1345
|
const keys = Object.keys(root || {}).slice(0, 4).join(", ");
|
|
1348
|
-
return keys ? `
|
|
1346
|
+
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
1349
1347
|
}
|
|
1350
|
-
|
|
1348
|
+
function newApiTokenQuota(data) {
|
|
1351
1349
|
const root = usageRoot(data);
|
|
1352
1350
|
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
1353
|
-
const quota = pickNumber(root, [
|
|
1354
|
-
|
|
1355
|
-
|
|
1351
|
+
const quota = pickNumber(root, [
|
|
1352
|
+
"quota",
|
|
1353
|
+
"limit",
|
|
1354
|
+
"total_quota",
|
|
1355
|
+
"totalQuota",
|
|
1356
|
+
"total_granted",
|
|
1357
|
+
"totalGranted"
|
|
1358
|
+
]);
|
|
1359
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used", "total_used", "totalUsed"]);
|
|
1360
|
+
let remaining = pickNumber(root, [
|
|
1361
|
+
"remain_quota",
|
|
1362
|
+
"remainQuota",
|
|
1363
|
+
"remaining",
|
|
1364
|
+
"balance",
|
|
1365
|
+
"total_available",
|
|
1366
|
+
"totalAvailable"
|
|
1367
|
+
]);
|
|
1356
1368
|
if (remaining === void 0 && quota !== void 0 && used !== void 0) {
|
|
1357
1369
|
remaining = Math.max(0, quota - used);
|
|
1358
1370
|
}
|
|
1371
|
+
return { unlimited, quota, used, remaining };
|
|
1372
|
+
}
|
|
1373
|
+
async function formatNewApiTokenLine(data) {
|
|
1374
|
+
const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
|
|
1359
1375
|
if (!unlimited && quota === void 0 && used === void 0 && remaining === void 0) {
|
|
1360
1376
|
throw new Error("NewAPI token usage payload has no quota fields");
|
|
1361
1377
|
}
|
|
1362
|
-
const parts =
|
|
1378
|
+
const parts = [];
|
|
1363
1379
|
if (unlimited) parts.push("unlimited");
|
|
1364
|
-
if (remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
1380
|
+
if (!unlimited && remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
1365
1381
|
if (used !== void 0 && quota !== void 0) {
|
|
1366
1382
|
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
1367
1383
|
} else if (used !== void 0) {
|
|
@@ -1369,13 +1385,13 @@ async function formatNewApiTokenLine(label, data) {
|
|
|
1369
1385
|
}
|
|
1370
1386
|
return parts.join(" | ");
|
|
1371
1387
|
}
|
|
1372
|
-
function formatOpenRouterLine(
|
|
1388
|
+
function formatOpenRouterLine(data) {
|
|
1373
1389
|
const root = usageRoot(data);
|
|
1374
1390
|
const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
|
|
1375
1391
|
const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
|
|
1376
1392
|
const used = pickNumber(root, ["usage", "total_usage", "spend"]);
|
|
1377
1393
|
const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
|
|
1378
|
-
const parts =
|
|
1394
|
+
const parts = [];
|
|
1379
1395
|
if (remaining !== void 0) parts.push(`balance ${formatMoney(remaining)}`);
|
|
1380
1396
|
if (used !== void 0 && limit !== void 0 && limit !== remaining) {
|
|
1381
1397
|
parts.push(`used ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
@@ -1383,18 +1399,61 @@ function formatOpenRouterLine(label, data) {
|
|
|
1383
1399
|
parts.push(`used ${formatMoney(used)}`);
|
|
1384
1400
|
}
|
|
1385
1401
|
if (reset) parts.push(`Reset ${reset}`);
|
|
1386
|
-
if (parts.length ===
|
|
1402
|
+
if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
|
|
1387
1403
|
return parts.join(" | ");
|
|
1388
1404
|
}
|
|
1389
1405
|
function formatOneApiBillingLine(limit, used) {
|
|
1390
|
-
|
|
1406
|
+
if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
|
|
1407
|
+
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
1408
|
+
}
|
|
1409
|
+
function hasSpendableOneApiLimit(limit) {
|
|
1410
|
+
return Number.isFinite(limit) && limit >= 0 && limit < ONE_API_HARD_LIMIT_SENTINEL_USD;
|
|
1411
|
+
}
|
|
1412
|
+
function effectiveClaudeCodeHubWindow(root, suffix) {
|
|
1413
|
+
const keyLimit = pickNumber(root, [`keyLimit${suffix}Usd`]);
|
|
1414
|
+
if (keyLimit !== void 0 && keyLimit > 0) {
|
|
1415
|
+
return { limit: keyLimit, used: pickNumber(root, [`keyCurrent${suffix}Usd`]) };
|
|
1416
|
+
}
|
|
1417
|
+
const userLimit = pickNumber(root, [`userLimit${suffix}Usd`]);
|
|
1418
|
+
return {
|
|
1419
|
+
limit: userLimit !== void 0 && userLimit > 0 ? userLimit : void 0,
|
|
1420
|
+
used: pickNumber(root, [`userCurrent${suffix}Usd`])
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function earliestDate(...values) {
|
|
1424
|
+
return values.map((value) => ({ value, time: Date.parse(value) })).filter((entry) => entry.value && Number.isFinite(entry.time)).sort((left, right) => left.time - right.time)[0]?.value;
|
|
1425
|
+
}
|
|
1426
|
+
function formatClaudeCodeHubLine(data) {
|
|
1427
|
+
const root = usageRoot(data);
|
|
1428
|
+
const windows = [
|
|
1429
|
+
["5h", "5h"],
|
|
1430
|
+
["D", "Daily"],
|
|
1431
|
+
["W", "Weekly"],
|
|
1432
|
+
["M", "Monthly"],
|
|
1433
|
+
["T", "Total"]
|
|
1434
|
+
];
|
|
1435
|
+
const parts = [];
|
|
1436
|
+
for (const [label, suffix] of windows) {
|
|
1437
|
+
const { limit, used } = effectiveClaudeCodeHubWindow(root, suffix);
|
|
1438
|
+
if (limit !== void 0 && used !== void 0) {
|
|
1439
|
+
parts.push(`${label} ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (parts.length === 0) {
|
|
1443
|
+
const totalUsed = pickNumber(root, ["keyCurrentTotalUsd", "userCurrentTotalUsd"]);
|
|
1444
|
+
if (totalUsed !== void 0) parts.push(`used ${formatMoney(totalUsed)}`);
|
|
1445
|
+
}
|
|
1446
|
+
const expires = shortDate(earliestDate(root?.expiresAt, root?.userExpiresAt));
|
|
1447
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
1448
|
+
if (parts.length === 0) throw new Error("Claude Code Hub quota payload has no quota fields");
|
|
1449
|
+
return parts.join(" | ");
|
|
1391
1450
|
}
|
|
1392
|
-
function formatQuotaLimitedLine(
|
|
1451
|
+
function formatQuotaLimitedLine(root) {
|
|
1393
1452
|
const quota = root?.quota || {};
|
|
1394
1453
|
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
1395
1454
|
const used = pickNumber(quota, ["used", "quota_used"]);
|
|
1396
1455
|
const remaining = pickNumber(quota, ["remaining"]) ?? pickNumber(root, ["remaining"]);
|
|
1397
|
-
const parts =
|
|
1456
|
+
const parts = [];
|
|
1398
1457
|
if (limit !== void 0 && used !== void 0) {
|
|
1399
1458
|
parts.push(`Q ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
1400
1459
|
} else if (remaining !== void 0) {
|
|
@@ -1411,11 +1470,11 @@ function formatQuotaLimitedLine(label, root) {
|
|
|
1411
1470
|
}
|
|
1412
1471
|
return parts.join(" | ");
|
|
1413
1472
|
}
|
|
1414
|
-
async function formatWalletLine(
|
|
1473
|
+
async function formatWalletLine(root) {
|
|
1415
1474
|
const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
|
|
1416
1475
|
const todayCost = pickNumber(root?.usage?.today, ["actual_cost", "cost"]);
|
|
1417
1476
|
const recentUsage = Array.isArray(root?.daily_usage) ? root.daily_usage.reduce((sum, day) => sum + (pickNumber(day, ["actual_cost", "cost"]) || 0), 0) : void 0;
|
|
1418
|
-
const parts =
|
|
1477
|
+
const parts = [];
|
|
1419
1478
|
if (balance !== void 0) parts.push(`balance ${formatMoney(balance)}`);
|
|
1420
1479
|
if (todayCost !== void 0) parts.push(`today ${formatMoney(todayCost)}`);
|
|
1421
1480
|
if (recentUsage !== void 0 && root.daily_usage.length > 0) {
|
|
@@ -1423,7 +1482,7 @@ async function formatWalletLine(label, root) {
|
|
|
1423
1482
|
}
|
|
1424
1483
|
return parts.join(" | ");
|
|
1425
1484
|
}
|
|
1426
|
-
function formatUsageLine(
|
|
1485
|
+
function formatUsageLine(root) {
|
|
1427
1486
|
const sub = root?.subscription || {};
|
|
1428
1487
|
const dailyLimit = pickNumber(sub, ["daily_limit_usd"]);
|
|
1429
1488
|
const dailyUsage = pickNumber(sub, ["daily_usage_usd"]);
|
|
@@ -1432,7 +1491,7 @@ function formatUsageLine(label, root) {
|
|
|
1432
1491
|
const monthlyLimit = pickNumber(sub, ["monthly_limit_usd"]);
|
|
1433
1492
|
const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
|
|
1434
1493
|
const expires = shortDate(sub.expires_at);
|
|
1435
|
-
const parts =
|
|
1494
|
+
const parts = [];
|
|
1436
1495
|
if (dailyLimit > 0 && dailyUsage !== void 0) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
|
|
1437
1496
|
if (weeklyLimit > 0 && weeklyUsage !== void 0) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
|
|
1438
1497
|
if (monthlyLimit > 0 && monthlyUsage !== void 0) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
|
|
@@ -1462,20 +1521,14 @@ async function fetchV1Usage(context) {
|
|
|
1462
1521
|
name: "v1 usage"
|
|
1463
1522
|
});
|
|
1464
1523
|
if (!hasV1UsageFields(usageRoot(json))) throw new Error("v1 usage payload has no usage fields");
|
|
1465
|
-
return usageResult(context, "v1-usage", await formatQuota(
|
|
1524
|
+
return usageResult(context, "v1-usage", await formatQuota(json), json);
|
|
1466
1525
|
}
|
|
1467
1526
|
async function fetchNewApiTokenUsage(context) {
|
|
1468
1527
|
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/usage/token/"), {
|
|
1469
1528
|
key: context.key,
|
|
1470
1529
|
name: "New API token usage"
|
|
1471
1530
|
});
|
|
1472
|
-
const
|
|
1473
|
-
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
1474
|
-
const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
|
|
1475
|
-
let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
|
|
1476
|
-
if (remaining === void 0 && quota !== void 0 && used !== void 0) {
|
|
1477
|
-
remaining = Math.max(0, quota - used);
|
|
1478
|
-
}
|
|
1531
|
+
const { quota, used, remaining } = newApiTokenQuota(json);
|
|
1479
1532
|
const scale = await newApiQuotaScale();
|
|
1480
1533
|
const quotaForWarning = scale ? quota / scale : quota;
|
|
1481
1534
|
const usedForWarning = scale ? used / scale : used;
|
|
@@ -1491,7 +1544,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
1491
1544
|
source: "newapi-token",
|
|
1492
1545
|
raw: json
|
|
1493
1546
|
};
|
|
1494
|
-
return usageResult(context, "newapi-token", await formatNewApiTokenLine(
|
|
1547
|
+
return usageResult(context, "newapi-token", await formatNewApiTokenLine(json), normalized);
|
|
1495
1548
|
}
|
|
1496
1549
|
async function fetchOneApiBillingUsage(context) {
|
|
1497
1550
|
const base = serviceRoot(context.baseUrl);
|
|
@@ -1509,13 +1562,16 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
1509
1562
|
throw new Error("One API billing payload has no quota fields");
|
|
1510
1563
|
}
|
|
1511
1564
|
const used = usageCents / 100;
|
|
1565
|
+
const spendableLimit = hasSpendableOneApiLimit(limit);
|
|
1512
1566
|
const normalized = {
|
|
1513
|
-
mode: "quota_limited",
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1567
|
+
mode: spendableLimit ? "quota_limited" : "unrestricted",
|
|
1568
|
+
...spendableLimit ? {
|
|
1569
|
+
quota: {
|
|
1570
|
+
limit,
|
|
1571
|
+
used,
|
|
1572
|
+
remaining: Math.max(0, limit - used)
|
|
1573
|
+
}
|
|
1574
|
+
} : { used },
|
|
1519
1575
|
unit: "USD",
|
|
1520
1576
|
source: "oneapi-billing",
|
|
1521
1577
|
raw: { subscription, usage }
|
|
@@ -1527,6 +1583,19 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
1527
1583
|
normalized
|
|
1528
1584
|
);
|
|
1529
1585
|
}
|
|
1586
|
+
async function fetchClaudeCodeHubUsage(context) {
|
|
1587
|
+
const base = cleanBaseUrl(context.baseUrl).replace(/\/api\/v1$/i, "").replace(/\/v1$/i, "");
|
|
1588
|
+
const json = await requestJson(joinUrl(base, "/api/v1/me/quota"), {
|
|
1589
|
+
key: context.key,
|
|
1590
|
+
name: "Claude Code Hub quota"
|
|
1591
|
+
});
|
|
1592
|
+
return usageResult(
|
|
1593
|
+
context,
|
|
1594
|
+
"claude-code-hub",
|
|
1595
|
+
formatClaudeCodeHubLine(json),
|
|
1596
|
+
json
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1530
1599
|
async function fetchOpenRouterUsage(context) {
|
|
1531
1600
|
const base = cleanBaseUrl(context.baseUrl).includes("/api/v1") ? cleanBaseUrl(context.baseUrl) : joinUrl(serviceRoot(context.baseUrl), "/api/v1");
|
|
1532
1601
|
const endpoints = [
|
|
@@ -1537,7 +1606,7 @@ async function fetchOpenRouterUsage(context) {
|
|
|
1537
1606
|
for (const endpoint of endpoints) {
|
|
1538
1607
|
try {
|
|
1539
1608
|
const json = await requestJson(endpoint.url, { key: context.key, name: endpoint.source });
|
|
1540
|
-
return usageResult(context, endpoint.source, formatOpenRouterLine(
|
|
1609
|
+
return usageResult(context, endpoint.source, formatOpenRouterLine(json), json);
|
|
1541
1610
|
} catch (error) {
|
|
1542
1611
|
lastError = error;
|
|
1543
1612
|
await debugLog({ source: endpoint.source, error: error.message });
|
|
@@ -1565,6 +1634,11 @@ var USAGE_ROUTES = {
|
|
|
1565
1634
|
id: "openrouter",
|
|
1566
1635
|
path: "/api/v1/key",
|
|
1567
1636
|
run: fetchOpenRouterUsage
|
|
1637
|
+
},
|
|
1638
|
+
"claude-code-hub": {
|
|
1639
|
+
id: "claude-code-hub",
|
|
1640
|
+
path: "/api/v1/me/quota",
|
|
1641
|
+
run: fetchClaudeCodeHubUsage
|
|
1568
1642
|
}
|
|
1569
1643
|
};
|
|
1570
1644
|
var CUSTOM_ROUTE_HELPERS = { requestJson, agentConfig };
|
|
@@ -1637,12 +1711,15 @@ async function usageRouteIds(context) {
|
|
|
1637
1711
|
"openai-compatible": ["v1-usage"],
|
|
1638
1712
|
"new-api": ["newapi-token"],
|
|
1639
1713
|
"one-api": ["oneapi-billing"],
|
|
1640
|
-
"
|
|
1714
|
+
"one-hub": ["oneapi-billing"],
|
|
1715
|
+
"done-hub": ["oneapi-billing"],
|
|
1716
|
+
"openrouter": ["openrouter"],
|
|
1717
|
+
"claude-code-hub": ["claude-code-hub"]
|
|
1641
1718
|
};
|
|
1642
1719
|
if (routes[preset]) return routes[preset];
|
|
1643
1720
|
if (preset !== "auto") return (await routeRegistry())[preset] ? [preset] : [];
|
|
1644
1721
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
1645
|
-
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
1722
|
+
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing", "claude-code-hub"];
|
|
1646
1723
|
return [.../* @__PURE__ */ new Set([...customIds, ...builtinIds])];
|
|
1647
1724
|
}
|
|
1648
1725
|
async function cachedUsageRoute(context, registry) {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// integrations/usage/opencode-plugin.mjs
|
|
2
2
|
import { queryProviderUsage } from "./core.mjs";
|
|
3
3
|
var DEFAULT_REFRESH_MS = 6e4;
|
|
4
|
-
function toastMessage(text) {
|
|
5
|
-
return text.replace(/^API \| /, "");
|
|
6
|
-
}
|
|
7
4
|
function firstString(value, keys) {
|
|
8
5
|
for (const key of keys) {
|
|
9
6
|
if (typeof value?.[key] === "string" && value[key].trim()) return value[key].trim();
|
|
@@ -38,7 +35,7 @@ var AgentToolsUsage = async ({ client }, options = {}) => {
|
|
|
38
35
|
await client.tui.showToast({
|
|
39
36
|
body: {
|
|
40
37
|
title: "Provider usage",
|
|
41
|
-
message:
|
|
38
|
+
message: text,
|
|
42
39
|
variant: "info",
|
|
43
40
|
duration: 8e3
|
|
44
41
|
}
|
|
@@ -4,9 +4,6 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
|
|
6
6
|
var SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
|
|
7
|
-
function toastMessage(text) {
|
|
8
|
-
return text.replace(/^API \| /, "");
|
|
9
|
-
}
|
|
10
7
|
function latestUsage() {
|
|
11
8
|
try {
|
|
12
9
|
const data = JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8"));
|
|
@@ -28,7 +25,7 @@ var tui = async (api) => {
|
|
|
28
25
|
const message = latestUsage();
|
|
29
26
|
api.ui.toast({
|
|
30
27
|
title: "Provider usage",
|
|
31
|
-
message: message
|
|
28
|
+
message: message || "Provider usage is not available yet",
|
|
32
29
|
variant: message ? "info" : "warning",
|
|
33
30
|
duration: 8e3
|
|
34
31
|
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Turns gateway payloads into the compact one-line usage message.
|
|
2
|
+
// Lines carry no branding prefix; each display surface adds its own context
|
|
3
|
+
// (hook name, statusline position, toast title) if it needs any.
|
|
2
4
|
|
|
3
5
|
import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
|
|
4
6
|
|
|
7
|
+
const ONE_API_HARD_LIMIT_SENTINEL_USD = 1_000_000;
|
|
8
|
+
|
|
5
9
|
export function pickNumber(obj, keys) {
|
|
6
10
|
for (const key of keys) {
|
|
7
11
|
const value = obj?.[key];
|
|
@@ -20,10 +24,6 @@ function formatMaybeMoney(value, unit = "USD") {
|
|
|
20
24
|
return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
function usageParts() {
|
|
24
|
-
return ["API"];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
27
|
async function formatNewApiQuota(value) {
|
|
28
28
|
const scale = await newApiQuotaScale();
|
|
29
29
|
if (Number.isFinite(scale) && scale > 0) return formatMoney(value / scale);
|
|
@@ -92,7 +92,7 @@ export function hasV1UsageFields(root) {
|
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export async function formatQuota(
|
|
95
|
+
export async function formatQuota(data) {
|
|
96
96
|
const root = usageRoot(data);
|
|
97
97
|
const unit = root?.unit || "USD";
|
|
98
98
|
const remaining = pickNumber(root, ["remaining"]);
|
|
@@ -100,37 +100,56 @@ export async function formatQuota(label, data) {
|
|
|
100
100
|
const used = pickNumber(root, ["total_usage", "used", "usage"]);
|
|
101
101
|
const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
|
|
102
102
|
|
|
103
|
-
if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(
|
|
104
|
-
if (isSubscriptionUsage(root)) return formatUsageLine(
|
|
105
|
-
if (isWalletUsage(root)) return await formatWalletLine(
|
|
106
|
-
if (remaining !== undefined) return formatUsageLine(
|
|
107
|
-
if (balance !== undefined) return `
|
|
103
|
+
if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(root);
|
|
104
|
+
if (isSubscriptionUsage(root)) return formatUsageLine(root);
|
|
105
|
+
if (isWalletUsage(root)) return await formatWalletLine(root);
|
|
106
|
+
if (remaining !== undefined) return formatUsageLine(root);
|
|
107
|
+
if (balance !== undefined) return `balance ${formatMaybeMoney(balance, unit)}`;
|
|
108
108
|
if (hardLimit !== undefined && used !== undefined) {
|
|
109
|
-
return `
|
|
109
|
+
return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
|
|
110
110
|
}
|
|
111
|
-
if (hardLimit !== undefined) return `
|
|
111
|
+
if (hardLimit !== undefined) return `total ${formatMaybeMoney(hardLimit, unit)}`;
|
|
112
112
|
|
|
113
113
|
const keys = Object.keys(root || {}).slice(0, 4).join(", ");
|
|
114
|
-
return keys ? `
|
|
114
|
+
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
export
|
|
117
|
+
export function newApiTokenQuota(data) {
|
|
118
118
|
const root = usageRoot(data);
|
|
119
119
|
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
120
|
-
const quota = pickNumber(root, [
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
const quota = pickNumber(root, [
|
|
121
|
+
"quota",
|
|
122
|
+
"limit",
|
|
123
|
+
"total_quota",
|
|
124
|
+
"totalQuota",
|
|
125
|
+
"total_granted",
|
|
126
|
+
"totalGranted",
|
|
127
|
+
]);
|
|
128
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used", "total_used", "totalUsed"]);
|
|
129
|
+
let remaining = pickNumber(root, [
|
|
130
|
+
"remain_quota",
|
|
131
|
+
"remainQuota",
|
|
132
|
+
"remaining",
|
|
133
|
+
"balance",
|
|
134
|
+
"total_available",
|
|
135
|
+
"totalAvailable",
|
|
136
|
+
]);
|
|
123
137
|
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
124
138
|
remaining = Math.max(0, quota - used);
|
|
125
139
|
}
|
|
140
|
+
return { unlimited, quota, used, remaining };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function formatNewApiTokenLine(data) {
|
|
144
|
+
const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
|
|
126
145
|
|
|
127
146
|
if (!unlimited && quota === undefined && used === undefined && remaining === undefined) {
|
|
128
147
|
throw new Error("NewAPI token usage payload has no quota fields");
|
|
129
148
|
}
|
|
130
149
|
|
|
131
|
-
const parts =
|
|
150
|
+
const parts = [];
|
|
132
151
|
if (unlimited) parts.push("unlimited");
|
|
133
|
-
if (remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
152
|
+
if (!unlimited && remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
134
153
|
if (used !== undefined && quota !== undefined) {
|
|
135
154
|
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
136
155
|
} else if (used !== undefined) {
|
|
@@ -139,13 +158,13 @@ export async function formatNewApiTokenLine(label, data) {
|
|
|
139
158
|
return parts.join(" | ");
|
|
140
159
|
}
|
|
141
160
|
|
|
142
|
-
export function formatOpenRouterLine(
|
|
161
|
+
export function formatOpenRouterLine(data) {
|
|
143
162
|
const root = usageRoot(data);
|
|
144
163
|
const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
|
|
145
164
|
const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
|
|
146
165
|
const used = pickNumber(root, ["usage", "total_usage", "spend"]);
|
|
147
166
|
const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
|
|
148
|
-
const parts =
|
|
167
|
+
const parts = [];
|
|
149
168
|
|
|
150
169
|
if (remaining !== undefined) parts.push(`balance ${formatMoney(remaining)}`);
|
|
151
170
|
if (used !== undefined && limit !== undefined && limit !== remaining) {
|
|
@@ -155,20 +174,73 @@ export function formatOpenRouterLine(label, data) {
|
|
|
155
174
|
}
|
|
156
175
|
if (reset) parts.push(`Reset ${reset}`);
|
|
157
176
|
|
|
158
|
-
if (parts.length ===
|
|
177
|
+
if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
|
|
159
178
|
return parts.join(" | ");
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
export function formatOneApiBillingLine(limit, used) {
|
|
163
|
-
|
|
182
|
+
if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
|
|
183
|
+
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function hasSpendableOneApiLimit(limit) {
|
|
187
|
+
return Number.isFinite(limit) && limit >= 0 && limit < ONE_API_HARD_LIMIT_SENTINEL_USD;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function effectiveClaudeCodeHubWindow(root, suffix) {
|
|
191
|
+
const keyLimit = pickNumber(root, [`keyLimit${suffix}Usd`]);
|
|
192
|
+
if (keyLimit !== undefined && keyLimit > 0) {
|
|
193
|
+
return { limit: keyLimit, used: pickNumber(root, [`keyCurrent${suffix}Usd`]) };
|
|
194
|
+
}
|
|
195
|
+
const userLimit = pickNumber(root, [`userLimit${suffix}Usd`]);
|
|
196
|
+
return {
|
|
197
|
+
limit: userLimit !== undefined && userLimit > 0 ? userLimit : undefined,
|
|
198
|
+
used: pickNumber(root, [`userCurrent${suffix}Usd`]),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function earliestDate(...values) {
|
|
203
|
+
return values
|
|
204
|
+
.map((value) => ({ value, time: Date.parse(value) }))
|
|
205
|
+
.filter((entry) => entry.value && Number.isFinite(entry.time))
|
|
206
|
+
.sort((left, right) => left.time - right.time)[0]?.value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function formatClaudeCodeHubLine(data) {
|
|
210
|
+
const root = usageRoot(data);
|
|
211
|
+
const windows = [
|
|
212
|
+
["5h", "5h"],
|
|
213
|
+
["D", "Daily"],
|
|
214
|
+
["W", "Weekly"],
|
|
215
|
+
["M", "Monthly"],
|
|
216
|
+
["T", "Total"],
|
|
217
|
+
];
|
|
218
|
+
const parts = [];
|
|
219
|
+
|
|
220
|
+
for (const [label, suffix] of windows) {
|
|
221
|
+
const { limit, used } = effectiveClaudeCodeHubWindow(root, suffix);
|
|
222
|
+
if (limit !== undefined && used !== undefined) {
|
|
223
|
+
parts.push(`${label} ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (parts.length === 0) {
|
|
228
|
+
const totalUsed = pickNumber(root, ["keyCurrentTotalUsd", "userCurrentTotalUsd"]);
|
|
229
|
+
if (totalUsed !== undefined) parts.push(`used ${formatMoney(totalUsed)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const expires = shortDate(earliestDate(root?.expiresAt, root?.userExpiresAt));
|
|
233
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
234
|
+
if (parts.length === 0) throw new Error("Claude Code Hub quota payload has no quota fields");
|
|
235
|
+
return parts.join(" | ");
|
|
164
236
|
}
|
|
165
237
|
|
|
166
|
-
function formatQuotaLimitedLine(
|
|
238
|
+
function formatQuotaLimitedLine(root) {
|
|
167
239
|
const quota = root?.quota || {};
|
|
168
240
|
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
169
241
|
const used = pickNumber(quota, ["used", "quota_used"]);
|
|
170
242
|
const remaining = pickNumber(quota, ["remaining"]) ?? pickNumber(root, ["remaining"]);
|
|
171
|
-
const parts =
|
|
243
|
+
const parts = [];
|
|
172
244
|
|
|
173
245
|
if (limit !== undefined && used !== undefined) {
|
|
174
246
|
parts.push(`Q ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
@@ -193,14 +265,14 @@ function formatQuotaLimitedLine(label, root) {
|
|
|
193
265
|
return parts.join(" | ");
|
|
194
266
|
}
|
|
195
267
|
|
|
196
|
-
async function formatWalletLine(
|
|
268
|
+
async function formatWalletLine(root) {
|
|
197
269
|
const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
|
|
198
270
|
const todayCost = pickNumber(root?.usage?.today, ["actual_cost", "cost"]);
|
|
199
271
|
const recentUsage = Array.isArray(root?.daily_usage)
|
|
200
272
|
? root.daily_usage.reduce((sum, day) => sum + (pickNumber(day, ["actual_cost", "cost"]) || 0), 0)
|
|
201
273
|
: undefined;
|
|
202
274
|
|
|
203
|
-
const parts =
|
|
275
|
+
const parts = [];
|
|
204
276
|
if (balance !== undefined) parts.push(`balance ${formatMoney(balance)}`);
|
|
205
277
|
if (todayCost !== undefined) parts.push(`today ${formatMoney(todayCost)}`);
|
|
206
278
|
if (recentUsage !== undefined && root.daily_usage.length > 0) {
|
|
@@ -209,7 +281,7 @@ async function formatWalletLine(label, root) {
|
|
|
209
281
|
return parts.join(" | ");
|
|
210
282
|
}
|
|
211
283
|
|
|
212
|
-
function formatUsageLine(
|
|
284
|
+
function formatUsageLine(root) {
|
|
213
285
|
const sub = root?.subscription || {};
|
|
214
286
|
const dailyLimit = pickNumber(sub, ["daily_limit_usd"]);
|
|
215
287
|
const dailyUsage = pickNumber(sub, ["daily_usage_usd"]);
|
|
@@ -219,7 +291,7 @@ function formatUsageLine(label, root) {
|
|
|
219
291
|
const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
|
|
220
292
|
const expires = shortDate(sub.expires_at);
|
|
221
293
|
|
|
222
|
-
const parts =
|
|
294
|
+
const parts = [];
|
|
223
295
|
if (dailyLimit > 0 && dailyUsage !== undefined) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
|
|
224
296
|
if (weeklyLimit > 0 && weeklyUsage !== undefined) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
|
|
225
297
|
if (monthlyLimit > 0 && monthlyUsage !== undefined) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
|
|
@@ -24,9 +24,12 @@ import {
|
|
|
24
24
|
usageRoot,
|
|
25
25
|
hasV1UsageFields,
|
|
26
26
|
formatQuota,
|
|
27
|
+
newApiTokenQuota,
|
|
27
28
|
formatNewApiTokenLine,
|
|
28
29
|
formatOneApiBillingLine,
|
|
30
|
+
hasSpendableOneApiLimit,
|
|
29
31
|
formatOpenRouterLine,
|
|
32
|
+
formatClaudeCodeHubLine,
|
|
30
33
|
} from "./format.mjs";
|
|
31
34
|
import { readRouteCache } from "./cache.mjs";
|
|
32
35
|
|
|
@@ -56,7 +59,7 @@ async function fetchV1Usage(context) {
|
|
|
56
59
|
name: "v1 usage",
|
|
57
60
|
});
|
|
58
61
|
if (!hasV1UsageFields(usageRoot(json))) throw new Error("v1 usage payload has no usage fields");
|
|
59
|
-
return usageResult(context, "v1-usage", await formatQuota(
|
|
62
|
+
return usageResult(context, "v1-usage", await formatQuota(json), json);
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
// New API exposes a read-only usage endpoint authenticated by the same relay
|
|
@@ -66,13 +69,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
66
69
|
key: context.key,
|
|
67
70
|
name: "New API token usage",
|
|
68
71
|
});
|
|
69
|
-
const
|
|
70
|
-
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
71
|
-
const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
|
|
72
|
-
let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
|
|
73
|
-
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
74
|
-
remaining = Math.max(0, quota - used);
|
|
75
|
-
}
|
|
72
|
+
const { quota, used, remaining } = newApiTokenQuota(json);
|
|
76
73
|
const scale = await newApiQuotaScale();
|
|
77
74
|
const quotaForWarning = scale ? quota / scale : quota;
|
|
78
75
|
const usedForWarning = scale ? used / scale : used;
|
|
@@ -88,7 +85,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
88
85
|
source: "newapi-token",
|
|
89
86
|
raw: json,
|
|
90
87
|
};
|
|
91
|
-
return usageResult(context, "newapi-token", await formatNewApiTokenLine(
|
|
88
|
+
return usageResult(context, "newapi-token", await formatNewApiTokenLine(json), normalized);
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
// One API's legacy OpenAI billing endpoints use the same relay API key as
|
|
@@ -109,13 +106,16 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
109
106
|
throw new Error("One API billing payload has no quota fields");
|
|
110
107
|
}
|
|
111
108
|
const used = usageCents / 100;
|
|
109
|
+
const spendableLimit = hasSpendableOneApiLimit(limit);
|
|
112
110
|
const normalized = {
|
|
113
|
-
mode: "quota_limited",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
mode: spendableLimit ? "quota_limited" : "unrestricted",
|
|
112
|
+
...(spendableLimit ? {
|
|
113
|
+
quota: {
|
|
114
|
+
limit,
|
|
115
|
+
used,
|
|
116
|
+
remaining: Math.max(0, limit - used),
|
|
117
|
+
},
|
|
118
|
+
} : { used }),
|
|
119
119
|
unit: "USD",
|
|
120
120
|
source: "oneapi-billing",
|
|
121
121
|
raw: { subscription, usage },
|
|
@@ -128,6 +128,24 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Claude Code Hub exposes self-scoped quota windows to the same API key used
|
|
132
|
+
// for model requests. The endpoint accepts the existing Bearer authentication.
|
|
133
|
+
async function fetchClaudeCodeHubUsage(context) {
|
|
134
|
+
const base = cleanBaseUrl(context.baseUrl)
|
|
135
|
+
.replace(/\/api\/v1$/i, "")
|
|
136
|
+
.replace(/\/v1$/i, "");
|
|
137
|
+
const json = await requestJson(joinUrl(base, "/api/v1/me/quota"), {
|
|
138
|
+
key: context.key,
|
|
139
|
+
name: "Claude Code Hub quota",
|
|
140
|
+
});
|
|
141
|
+
return usageResult(
|
|
142
|
+
context,
|
|
143
|
+
"claude-code-hub",
|
|
144
|
+
formatClaudeCodeHubLine(json),
|
|
145
|
+
json
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
// OpenRouter exposes normal API-key usage at /api/v1/key. Some accounts also
|
|
132
150
|
// expose credits at /api/v1/credits; keep this route isolated because
|
|
133
151
|
// OpenRouter's base URL already includes /api/v1, unlike New API.
|
|
@@ -143,7 +161,7 @@ async function fetchOpenRouterUsage(context) {
|
|
|
143
161
|
for (const endpoint of endpoints) {
|
|
144
162
|
try {
|
|
145
163
|
const json = await requestJson(endpoint.url, { key: context.key, name: endpoint.source });
|
|
146
|
-
return usageResult(context, endpoint.source, formatOpenRouterLine(
|
|
164
|
+
return usageResult(context, endpoint.source, formatOpenRouterLine(json), json);
|
|
147
165
|
} catch (error) {
|
|
148
166
|
lastError = error;
|
|
149
167
|
await debugLog({ source: endpoint.source, error: error.message });
|
|
@@ -173,6 +191,11 @@ const USAGE_ROUTES = {
|
|
|
173
191
|
path: "/api/v1/key",
|
|
174
192
|
run: fetchOpenRouterUsage,
|
|
175
193
|
},
|
|
194
|
+
"claude-code-hub": {
|
|
195
|
+
id: "claude-code-hub",
|
|
196
|
+
path: "/api/v1/me/quota",
|
|
197
|
+
run: fetchClaudeCodeHubUsage,
|
|
198
|
+
},
|
|
176
199
|
};
|
|
177
200
|
|
|
178
201
|
// User-authored gateway routes, declared in config.jsonc:
|
|
@@ -256,7 +279,8 @@ async function routeRegistry() {
|
|
|
256
279
|
return registry;
|
|
257
280
|
}
|
|
258
281
|
|
|
259
|
-
// Presets select API-key usage protocols
|
|
282
|
+
// Presets primarily select API-key usage protocols. Brand aliases map to a
|
|
283
|
+
// shared protocol when the upstream gateway exposes the same endpoints.
|
|
260
284
|
async function usageRouteIds(context) {
|
|
261
285
|
const preset = await usagePreset();
|
|
262
286
|
const routes = {
|
|
@@ -264,7 +288,10 @@ async function usageRouteIds(context) {
|
|
|
264
288
|
"openai-compatible": ["v1-usage"],
|
|
265
289
|
"new-api": ["newapi-token"],
|
|
266
290
|
"one-api": ["oneapi-billing"],
|
|
291
|
+
"one-hub": ["oneapi-billing"],
|
|
292
|
+
"done-hub": ["oneapi-billing"],
|
|
267
293
|
"openrouter": ["openrouter"],
|
|
294
|
+
"claude-code-hub": ["claude-code-hub"],
|
|
268
295
|
};
|
|
269
296
|
if (routes[preset]) return routes[preset];
|
|
270
297
|
|
|
@@ -275,7 +302,7 @@ async function usageRouteIds(context) {
|
|
|
275
302
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
276
303
|
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai")
|
|
277
304
|
? ["openrouter"]
|
|
278
|
-
: ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
305
|
+
: ["v1-usage", "newapi-token", "oneapi-billing", "claude-code-hub"];
|
|
279
306
|
return [...new Set([...customIds, ...builtinIds])];
|
|
280
307
|
}
|
|
281
308
|
|
|
@@ -2,10 +2,6 @@ import { queryProviderUsage } from "./core.mjs";
|
|
|
2
2
|
|
|
3
3
|
const DEFAULT_REFRESH_MS = 60_000;
|
|
4
4
|
|
|
5
|
-
function toastMessage(text) {
|
|
6
|
-
return text.replace(/^API \| /, "");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
5
|
function firstString(value, keys) {
|
|
10
6
|
for (const key of keys) {
|
|
11
7
|
if (typeof value?.[key] === "string" && value[key].trim()) return value[key].trim();
|
|
@@ -49,7 +45,7 @@ export const AgentToolsUsage = async ({ client }, options = {}) => {
|
|
|
49
45
|
await client.tui.showToast({
|
|
50
46
|
body: {
|
|
51
47
|
title: "Provider usage",
|
|
52
|
-
message:
|
|
48
|
+
message: text,
|
|
53
49
|
variant: "info",
|
|
54
50
|
duration: 8000,
|
|
55
51
|
},
|
|
@@ -5,10 +5,6 @@ import { join } from "node:path";
|
|
|
5
5
|
const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
|
|
6
6
|
const SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
|
|
7
7
|
|
|
8
|
-
function toastMessage(text) {
|
|
9
|
-
return text.replace(/^API \| /, "");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
function latestUsage() {
|
|
13
9
|
try {
|
|
14
10
|
const data = JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8"));
|
|
@@ -33,7 +29,7 @@ const tui = async (api) => {
|
|
|
33
29
|
const message = latestUsage();
|
|
34
30
|
api.ui.toast({
|
|
35
31
|
title: "Provider usage",
|
|
36
|
-
message: message
|
|
32
|
+
message: message || "Provider usage is not available yet",
|
|
37
33
|
variant: message ? "info" : "warning",
|
|
38
34
|
duration: 8000,
|
|
39
35
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairyou/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Reusable Agent Skills and installable integrations (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"build": "node scripts/build.mjs",
|
|
33
33
|
"prepare": "npm run build",
|
|
34
34
|
"test": "node --test tests/*.test.mjs",
|
|
35
|
-
"release": "node scripts/release.mjs"
|
|
35
|
+
"release": "node scripts/release.mjs --target=registry",
|
|
36
|
+
"release:github": "node scripts/release.mjs --target=github"
|
|
36
37
|
},
|
|
37
38
|
"bin": {
|
|
38
39
|
"agent-tools": "./scripts/install.mjs"
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Internal registry publisher used by release.mjs.
|
|
3
|
+
// Publishes to npmjs or an npm-compatible private registry without changing the version.
|
|
4
|
+
// Credentials persist in ~/.agent-tools/publish.npmrc, leaving ~/.npmrc untouched.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
13
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
|
|
14
|
+
const PACKAGE_NAME = packageJson.name;
|
|
15
|
+
const PUBLISH_NPMRC = path.join(
|
|
16
|
+
process.env.AGENT_TOOLS_HOME || path.join(os.homedir(), ".agent-tools"),
|
|
17
|
+
"publish.npmrc"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
let registryOverride;
|
|
21
|
+
let authType;
|
|
22
|
+
let dryRun = false;
|
|
23
|
+
let authOnly = false;
|
|
24
|
+
for (const arg of process.argv.slice(2)) {
|
|
25
|
+
if (arg === "--dry-run") dryRun = true;
|
|
26
|
+
else if (arg === "--auth-only") authOnly = true;
|
|
27
|
+
else if (arg.startsWith("--registry=")) registryOverride = arg.slice("--registry=".length);
|
|
28
|
+
else if (arg.startsWith("--auth-type=")) authType = arg.slice("--auth-type=".length);
|
|
29
|
+
else {
|
|
30
|
+
console.error(
|
|
31
|
+
`Unknown argument: ${arg} ` +
|
|
32
|
+
`(expected --registry=<url>, --auth-type=<type>, --auth-only, or --dry-run)`
|
|
33
|
+
);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
|
|
39
|
+
console.error("package.json must define a non-empty name before publishing.");
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runNpm(args, { capture = false, allowFailure = false } = {}) {
|
|
44
|
+
const npmExecPath = process.env.npm_execpath;
|
|
45
|
+
const command = npmExecPath ? process.execPath : process.platform === "win32" ? "npm.cmd" : "npm";
|
|
46
|
+
const commandArgs = npmExecPath ? [npmExecPath, ...args] : args;
|
|
47
|
+
const result = spawnSync(command, commandArgs, {
|
|
48
|
+
cwd: ROOT,
|
|
49
|
+
encoding: capture ? "utf8" : undefined,
|
|
50
|
+
stdio: capture ? "pipe" : "inherit",
|
|
51
|
+
});
|
|
52
|
+
if (result.error) throw result.error;
|
|
53
|
+
if (!allowFailure && result.status !== 0) process.exit(result.status ?? 1);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readNpmConfig(key) {
|
|
58
|
+
const result = runNpm(["config", "get", key], { capture: true });
|
|
59
|
+
const value = result.stdout.trim();
|
|
60
|
+
return value && value !== "undefined" && value !== "null" ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const packageScope = PACKAGE_NAME.match(/^(@[^/]+)\//)?.[1];
|
|
64
|
+
const registry =
|
|
65
|
+
registryOverride ||
|
|
66
|
+
packageJson.publishConfig?.registry ||
|
|
67
|
+
(packageScope && readNpmConfig(`${packageScope}:registry`)) ||
|
|
68
|
+
readNpmConfig("registry");
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(registry);
|
|
72
|
+
if (!["http:", "https:"].includes(url.protocol)) throw new Error("unsupported protocol");
|
|
73
|
+
} catch {
|
|
74
|
+
console.error("The resolved npm registry must be a valid HTTP(S) URL.");
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`Package: ${PACKAGE_NAME}@${packageJson.version}`);
|
|
79
|
+
console.log(`Registry: ${registry}`);
|
|
80
|
+
if (new URL(registry).hostname === "registry.npmjs.org") {
|
|
81
|
+
console.log("Note: npmjs releases normally go through `npm run release:github` (OIDC).");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
runNpm(["pack", "--dry-run"]);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fs.mkdirSync(path.dirname(PUBLISH_NPMRC), { recursive: true, mode: 0o700 });
|
|
90
|
+
if (!fs.existsSync(PUBLISH_NPMRC)) fs.writeFileSync(PUBLISH_NPMRC, "", { mode: 0o600 });
|
|
91
|
+
|
|
92
|
+
const npmConfigArgs = [`--registry=${registry}`, `--userconfig=${PUBLISH_NPMRC}`];
|
|
93
|
+
const whoami = runNpm(["whoami", ...npmConfigArgs], { capture: true, allowFailure: true });
|
|
94
|
+
if (whoami.status !== 0) {
|
|
95
|
+
console.log(`Authentication required. Credentials will be saved to ${PUBLISH_NPMRC}`);
|
|
96
|
+
const loginArgs = ["login", ...npmConfigArgs];
|
|
97
|
+
if (authType) loginArgs.push(`--auth-type=${authType}`);
|
|
98
|
+
runNpm(loginArgs);
|
|
99
|
+
runNpm(["whoami", ...npmConfigArgs]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (authOnly) {
|
|
103
|
+
console.log(`Authenticated for ${registry}`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
runNpm(["publish", ...npmConfigArgs]);
|
|
108
|
+
console.log(`\nPublished ${PACKAGE_NAME}@${packageJson.version} to ${registry}`);
|
package/scripts/release.mjs
CHANGED
|
@@ -2,60 +2,153 @@
|
|
|
2
2
|
// Release helper for this package.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// npm run release [-- minor|major]
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// Release when scripts/ integrations/ config.default.jsonc or
|
|
14
|
-
// package.json deps change — npx users only get these from the published package.
|
|
15
|
-
// (skills/ ships from GitHub via `npx skills add`; no release needed.)
|
|
5
|
+
// npm run release [-- patch|minor|major]
|
|
6
|
+
// Test, bump, publish to the configured npm registry, then push the commit and v* tag.
|
|
7
|
+
// npm run release:github [-- patch|minor|major]
|
|
8
|
+
// Test, bump, and push the commit and v* tag; GitHub Actions publishes the package.
|
|
9
|
+
// npm run release -- --publish-only [--registry=<url>] [--auth-type=legacy]
|
|
10
|
+
// Publish the current version only; do not test, bump, commit, tag, or push.
|
|
11
|
+
// npm run release -- --dry-run
|
|
12
|
+
// Test and preview the package; do not authenticate, bump, publish, or push.
|
|
16
13
|
|
|
17
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
18
|
|
|
19
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
20
|
+
const PUBLISH_SCRIPT = path.join(ROOT, "scripts", "publish.mjs");
|
|
19
21
|
const BUMPS = ["patch", "minor", "major"];
|
|
20
22
|
|
|
23
|
+
let target = "registry";
|
|
21
24
|
let bump = "patch";
|
|
25
|
+
let bumpSpecified = false;
|
|
22
26
|
let dryRun = false;
|
|
27
|
+
let publishOnly = false;
|
|
28
|
+
const publishArgs = [];
|
|
23
29
|
for (const arg of process.argv.slice(2)) {
|
|
24
30
|
if (arg === "--dry-run") dryRun = true;
|
|
25
|
-
else if (
|
|
26
|
-
else
|
|
27
|
-
|
|
31
|
+
else if (arg === "--publish-only") publishOnly = true;
|
|
32
|
+
else if (arg.startsWith("--target=")) target = arg.slice("--target=".length);
|
|
33
|
+
else if (arg.startsWith("--registry=") || arg.startsWith("--auth-type=")) {
|
|
34
|
+
publishArgs.push(arg);
|
|
35
|
+
} else if (BUMPS.includes(arg)) {
|
|
36
|
+
bump = arg;
|
|
37
|
+
bumpSpecified = true;
|
|
38
|
+
} else {
|
|
39
|
+
console.error(
|
|
40
|
+
`Unknown argument: ${arg} ` +
|
|
41
|
+
`(expected ${BUMPS.join("|")}, --publish-only, --dry-run, ` +
|
|
42
|
+
`--registry=<url>, or --auth-type=<type>)`
|
|
43
|
+
);
|
|
28
44
|
process.exit(2);
|
|
29
45
|
}
|
|
30
46
|
}
|
|
31
47
|
|
|
32
|
-
|
|
33
|
-
console.
|
|
34
|
-
|
|
48
|
+
if (!["registry", "github"].includes(target)) {
|
|
49
|
+
console.error(`Unknown release target: ${target} (expected registry or github)`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
if (target === "github" && publishArgs.length > 0) {
|
|
53
|
+
console.error("--registry and --auth-type only apply to registry releases.");
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
if (target === "github" && publishOnly) {
|
|
57
|
+
console.error("--publish-only only applies to registry releases.");
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
if (publishOnly && (dryRun || bumpSpecified)) {
|
|
61
|
+
console.error("--publish-only cannot be combined with a version bump or --dry-run.");
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function run(command, args, { label, onFailure } = {}) {
|
|
66
|
+
console.log(`\n> ${label || [command, ...args].join(" ")}`);
|
|
67
|
+
const result = spawnSync(command, args, { cwd: ROOT, stdio: "inherit" });
|
|
68
|
+
if (result.error) {
|
|
69
|
+
console.error(`\nAborted: ${result.error.message}`);
|
|
70
|
+
onFailure?.();
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
35
73
|
if (result.status !== 0) {
|
|
36
|
-
console.error(`\nAborted:
|
|
74
|
+
console.error(`\nAborted: command exited with ${result.status}.`);
|
|
75
|
+
onFailure?.();
|
|
37
76
|
process.exit(result.status ?? 1);
|
|
38
77
|
}
|
|
39
78
|
}
|
|
40
79
|
|
|
41
|
-
|
|
80
|
+
function runNpm(args) {
|
|
81
|
+
const npmExecPath = process.env.npm_execpath;
|
|
82
|
+
if (npmExecPath) {
|
|
83
|
+
run(process.execPath, [npmExecPath, ...args], { label: `npm ${args.join(" ")}` });
|
|
84
|
+
} else {
|
|
85
|
+
run(process.platform === "win32" ? "npm.cmd" : "npm", args);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printRecovery(commands) {
|
|
90
|
+
console.error("\nResume without creating another version:");
|
|
91
|
+
for (const command of commands) console.error(` ${command}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const status = spawnSync("git", ["status", "--porcelain"], { cwd: ROOT, encoding: "utf8" });
|
|
95
|
+
if (status.error || status.status !== 0) {
|
|
96
|
+
console.error(status.error?.message || status.stderr || "Unable to read git status.");
|
|
97
|
+
process.exit(status.status ?? 1);
|
|
98
|
+
}
|
|
42
99
|
if (!dryRun && status.stdout.trim() !== "") {
|
|
43
|
-
console.error("Working tree is not clean
|
|
100
|
+
console.error("Working tree is not clean - commit or stash first:\n" + status.stdout);
|
|
44
101
|
process.exit(1);
|
|
45
102
|
}
|
|
46
103
|
|
|
47
|
-
|
|
104
|
+
if (publishOnly) {
|
|
105
|
+
run(process.execPath, [PUBLISH_SCRIPT, ...publishArgs], {
|
|
106
|
+
label: `node scripts/publish.mjs ${publishArgs.join(" ")}`.trimEnd(),
|
|
107
|
+
});
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!dryRun && target === "registry") {
|
|
112
|
+
run(process.execPath, [PUBLISH_SCRIPT, "--auth-only", ...publishArgs], {
|
|
113
|
+
label: `node scripts/publish.mjs --auth-only ${publishArgs.join(" ")}`.trimEnd(),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
runNpm(["test"]);
|
|
48
117
|
|
|
49
118
|
if (dryRun) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.log("\nDry run complete. No version bump, nothing pushed.");
|
|
119
|
+
runNpm(["pack", "--dry-run"]);
|
|
120
|
+
console.log("\nDry run complete. No authentication, version bump, publish, or push.");
|
|
53
121
|
process.exit(0);
|
|
54
122
|
}
|
|
55
123
|
|
|
56
|
-
|
|
57
|
-
|
|
124
|
+
const versionArgs = ["version", bump, "--tag-version-prefix=v"];
|
|
125
|
+
if (target === "registry") versionArgs.push("--message=%s [skip ci]");
|
|
126
|
+
runNpm(versionArgs);
|
|
127
|
+
const version = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")).version;
|
|
128
|
+
const tag = `v${version}`;
|
|
58
129
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
130
|
+
if (target === "registry") {
|
|
131
|
+
run(process.execPath, [PUBLISH_SCRIPT, ...publishArgs], {
|
|
132
|
+
label: `node scripts/publish.mjs ${publishArgs.join(" ")}`.trimEnd(),
|
|
133
|
+
onFailure() {
|
|
134
|
+
const suffix = publishArgs.length > 0 ? ` ${publishArgs.join(" ")}` : "";
|
|
135
|
+
printRecovery([
|
|
136
|
+
`npm run release -- --publish-only${suffix}`,
|
|
137
|
+
"git push --follow-tags",
|
|
138
|
+
]);
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
run("git", ["push", "--follow-tags"], {
|
|
142
|
+
onFailure() {
|
|
143
|
+
printRecovery(["git push --follow-tags"]);
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
console.log(`\nReleased ${tag} to the configured npm registry.`);
|
|
147
|
+
} else {
|
|
148
|
+
run("git", ["push", "--follow-tags"], {
|
|
149
|
+
onFailure() {
|
|
150
|
+
printRecovery(["git push --follow-tags"]);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
console.log(`\nPushed ${tag}. GitHub Actions will publish it.`);
|
|
154
|
+
}
|