@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 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
- - Usage: `/at-commit [<language>]` — language for the commit description (Conventional Commits tokens stay in English)
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
- - Usage: `/at-review [--fix] [<pr|branch|path>]` — reports findings; `--fix` also applies them
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
- - Usage: `/at-simplify [<pr|branch|path>]`
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 | API | balance $362 | today $61.7 | 30d $566
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 Sub2API, One API, New API, and
113
- OpenRouter. Compatibility depends on the gateway version and whether the
114
- corresponding usage endpoint is enabled.
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
- "preset": "auto", // auto | sub2api | one-api | new-api | openrouter | <custom-route-id>
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
- # Relay plan quota.
156
- API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
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 balance.
159
- API | balance $362 | today $61.7 | 30d $566
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 plan limits; `Exp` is
163
- the plan expiry; `balance` is wallet credit; `today` and `30d` are API spend.
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: `API | balance ¥${me?.data?.balance}` };
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
- - 用法: `/at-commit [<语言>]` — 指定 commit description 的语言(Conventional Commits 语法部分保持英文)
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
- - 用法: `/at-review [--fix] [<pr|分支|路径>]` — 输出审查结果; 加 `--fix` 则同时应用修复
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
- - 用法: `/at-simplify [<pr|分支|路径>]`
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 | API | balance $362 | today $61.7 | 30d $566
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 与 OpenRouter 提供的 API Key 用量接口.
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
- "preset": "auto", // auto | sub2api | one-api | new-api | openrouter | <自定义 route id>
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
- API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
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
- API | balance $362 | today $61.7 | 30d $566
164
+ # 钱包与近期消耗 (sub2api).
165
+ balance $362 | today $61.7 | 30d $566
151
166
  ```
152
167
 
153
- 字段含义: `D/W/M` 是日/周/月套餐消耗与上限, `Exp` 是套餐到期日,
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: `API | balance ¥${me?.data?.balance}` };
210
+ return { text: `balance ¥${me?.data?.balance}` };
196
211
  }
197
212
  ```
198
213
 
@@ -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(label, data) {
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(label, root);
1339
- if (isSubscriptionUsage(root)) return formatUsageLine(label, root);
1340
- if (isWalletUsage(root)) return await formatWalletLine(label, root);
1341
- if (remaining !== void 0) return formatUsageLine(label, root);
1342
- if (balance !== void 0) return `API | balance ${formatMaybeMoney(balance, unit)}`;
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 `API | remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
1342
+ return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
1345
1343
  }
1346
- if (hardLimit !== void 0) return `API | total ${formatMaybeMoney(hardLimit, unit)}`;
1344
+ if (hardLimit !== void 0) return `total ${formatMaybeMoney(hardLimit, unit)}`;
1347
1345
  const keys = Object.keys(root || {}).slice(0, 4).join(", ");
1348
- return keys ? `API | received (${keys})` : `API | checked ${unit}`;
1346
+ return keys ? `received (${keys})` : `checked ${unit}`;
1349
1347
  }
1350
- async function formatNewApiTokenLine(label, data) {
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, ["quota", "limit", "total_quota", "totalQuota"]);
1354
- const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
1355
- let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
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 = usageParts();
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(label, data) {
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 = usageParts();
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 === 1) throw new Error("OpenRouter payload has no usage fields");
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
- return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
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(label, root) {
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 = usageParts();
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(label, root) {
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 = usageParts();
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(label, root) {
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 = usageParts();
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(context.label, json), json);
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 root = usageRoot(json);
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(context.label, json), normalized);
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
- quota: {
1515
- limit,
1516
- used,
1517
- remaining: Math.max(0, limit - used)
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("OpenRouter", json), json);
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
- "openrouter": ["openrouter"]
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: toastMessage(text),
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 ? toastMessage(message) : "Provider usage is not available yet",
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(label, data) {
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(label, root);
104
- if (isSubscriptionUsage(root)) return formatUsageLine(label, root);
105
- if (isWalletUsage(root)) return await formatWalletLine(label, root);
106
- if (remaining !== undefined) return formatUsageLine(label, root);
107
- if (balance !== undefined) return `API | balance ${formatMaybeMoney(balance, unit)}`;
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 `API | remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
109
+ return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
110
110
  }
111
- if (hardLimit !== undefined) return `API | total ${formatMaybeMoney(hardLimit, unit)}`;
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 ? `API | received (${keys})` : `API | checked ${unit}`;
114
+ return keys ? `received (${keys})` : `checked ${unit}`;
115
115
  }
116
116
 
117
- export async function formatNewApiTokenLine(label, data) {
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, ["quota", "limit", "total_quota", "totalQuota"]);
121
- const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
122
- let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
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 = usageParts();
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(label, data) {
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 = usageParts();
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 === 1) throw new Error("OpenRouter payload has no usage fields");
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
- return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
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(label, root) {
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 = usageParts();
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(label, root) {
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 = usageParts();
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(label, root) {
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 = usageParts();
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(context.label, json), json);
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 root = usageRoot(json);
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(context.label, json), normalized);
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
- quota: {
115
- limit,
116
- used,
117
- remaining: Math.max(0, limit - used),
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("OpenRouter", json), json);
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, not hosted gateway brands.
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: toastMessage(text),
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 ? toastMessage(message) : "Provider usage is not available yet",
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.7.4",
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}`);
@@ -2,60 +2,153 @@
2
2
  // Release helper for this package.
3
3
  //
4
4
  // Usage:
5
- // npm run release [-- minor|major] # default: patch
6
- // npm run release -- --dry-run # npm test + preview tarball, no bump/push
7
- //
8
- // Local steps: clean-tree check -> npm test -> npm version -> git push --follow-tags.
9
- // Publishing runs on GitHub Actions via npm trusted publishing (OIDC, no
10
- // tokens, no npm login): .github/workflows/release.yml publishes when the
11
- // version tag lands.
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 (BUMPS.includes(arg)) bump = arg;
26
- else {
27
- console.error(`Unknown argument: ${arg} (expected ${BUMPS.join("|")} or --dry-run)`);
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
- function run(command) {
33
- console.log(`\n> ${command}`);
34
- const result = spawnSync(command, { stdio: "inherit", shell: true });
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: \`${command}\` exited with ${result.status}.`);
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
- const status = spawnSync("git status --porcelain", { shell: true, encoding: "utf8" });
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 commit or stash first:\n" + status.stdout);
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
- run("npm test");
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
- // Shows exactly which files would ship; verify nothing is missing/extra.
51
- run("npm pack --dry-run");
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
- run(`npm version ${bump}`);
57
- run("git push --follow-tags");
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
- console.log(
60
- "\nTag pushed. GitHub Actions publishes it: https://github.com/kairyou/agent-tools/actions"
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
+ }