@kairyou/agent-tools 0.8.0 → 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
 
@@ -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.
164
+ # Plan limits (sub2api / openai-compatible).
156
165
  D $0.0/$100 | W $0.0/$300 | Exp 07-08
157
166
 
158
- # Wallet balance.
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
172
+
173
+ # Wallet and recent spend (sub2api).
159
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
 
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
 
@@ -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
- # 中转套餐额度.
155
+ # 套餐限额 (sub2api / openai-compatible).
147
156
  D $0.0/$100 | W $0.0/$300 | Exp 07-08
148
157
 
149
- # 钱包余额.
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
163
+
164
+ # 钱包与近期消耗 (sub2api).
150
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
  #### 自定义网关路由
@@ -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];
@@ -1344,21 +1345,39 @@ async function formatQuota(data) {
1344
1345
  const keys = Object.keys(root || {}).slice(0, 4).join(", ");
1345
1346
  return keys ? `received (${keys})` : `checked ${unit}`;
1346
1347
  }
1347
- async function formatNewApiTokenLine(data) {
1348
+ function newApiTokenQuota(data) {
1348
1349
  const root = usageRoot(data);
1349
1350
  const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
1350
- const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
1351
- const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
1352
- 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
+ ]);
1353
1368
  if (remaining === void 0 && quota !== void 0 && used !== void 0) {
1354
1369
  remaining = Math.max(0, quota - used);
1355
1370
  }
1371
+ return { unlimited, quota, used, remaining };
1372
+ }
1373
+ async function formatNewApiTokenLine(data) {
1374
+ const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
1356
1375
  if (!unlimited && quota === void 0 && used === void 0 && remaining === void 0) {
1357
1376
  throw new Error("NewAPI token usage payload has no quota fields");
1358
1377
  }
1359
1378
  const parts = [];
1360
1379
  if (unlimited) parts.push("unlimited");
1361
- if (remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
1380
+ if (!unlimited && remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
1362
1381
  if (used !== void 0 && quota !== void 0) {
1363
1382
  parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
1364
1383
  } else if (used !== void 0) {
@@ -1384,8 +1403,51 @@ function formatOpenRouterLine(data) {
1384
1403
  return parts.join(" | ");
1385
1404
  }
1386
1405
  function formatOneApiBillingLine(limit, used) {
1406
+ if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
1387
1407
  return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
1388
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(" | ");
1450
+ }
1389
1451
  function formatQuotaLimitedLine(root) {
1390
1452
  const quota = root?.quota || {};
1391
1453
  const limit = pickNumber(quota, ["limit", "quota"]);
@@ -1466,13 +1528,7 @@ async function fetchNewApiTokenUsage(context) {
1466
1528
  key: context.key,
1467
1529
  name: "New API token usage"
1468
1530
  });
1469
- const root = usageRoot(json);
1470
- const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
1471
- const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
1472
- let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
1473
- if (remaining === void 0 && quota !== void 0 && used !== void 0) {
1474
- remaining = Math.max(0, quota - used);
1475
- }
1531
+ const { quota, used, remaining } = newApiTokenQuota(json);
1476
1532
  const scale = await newApiQuotaScale();
1477
1533
  const quotaForWarning = scale ? quota / scale : quota;
1478
1534
  const usedForWarning = scale ? used / scale : used;
@@ -1506,13 +1562,16 @@ async function fetchOneApiBillingUsage(context) {
1506
1562
  throw new Error("One API billing payload has no quota fields");
1507
1563
  }
1508
1564
  const used = usageCents / 100;
1565
+ const spendableLimit = hasSpendableOneApiLimit(limit);
1509
1566
  const normalized = {
1510
- mode: "quota_limited",
1511
- quota: {
1512
- limit,
1513
- used,
1514
- remaining: Math.max(0, limit - used)
1515
- },
1567
+ mode: spendableLimit ? "quota_limited" : "unrestricted",
1568
+ ...spendableLimit ? {
1569
+ quota: {
1570
+ limit,
1571
+ used,
1572
+ remaining: Math.max(0, limit - used)
1573
+ }
1574
+ } : { used },
1516
1575
  unit: "USD",
1517
1576
  source: "oneapi-billing",
1518
1577
  raw: { subscription, usage }
@@ -1524,6 +1583,19 @@ async function fetchOneApiBillingUsage(context) {
1524
1583
  normalized
1525
1584
  );
1526
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
+ }
1527
1599
  async function fetchOpenRouterUsage(context) {
1528
1600
  const base = cleanBaseUrl(context.baseUrl).includes("/api/v1") ? cleanBaseUrl(context.baseUrl) : joinUrl(serviceRoot(context.baseUrl), "/api/v1");
1529
1601
  const endpoints = [
@@ -1562,6 +1634,11 @@ var USAGE_ROUTES = {
1562
1634
  id: "openrouter",
1563
1635
  path: "/api/v1/key",
1564
1636
  run: fetchOpenRouterUsage
1637
+ },
1638
+ "claude-code-hub": {
1639
+ id: "claude-code-hub",
1640
+ path: "/api/v1/me/quota",
1641
+ run: fetchClaudeCodeHubUsage
1565
1642
  }
1566
1643
  };
1567
1644
  var CUSTOM_ROUTE_HELPERS = { requestJson, agentConfig };
@@ -1634,12 +1711,15 @@ async function usageRouteIds(context) {
1634
1711
  "openai-compatible": ["v1-usage"],
1635
1712
  "new-api": ["newapi-token"],
1636
1713
  "one-api": ["oneapi-billing"],
1637
- "openrouter": ["openrouter"]
1714
+ "one-hub": ["oneapi-billing"],
1715
+ "done-hub": ["oneapi-billing"],
1716
+ "openrouter": ["openrouter"],
1717
+ "claude-code-hub": ["claude-code-hub"]
1638
1718
  };
1639
1719
  if (routes[preset]) return routes[preset];
1640
1720
  if (preset !== "auto") return (await routeRegistry())[preset] ? [preset] : [];
1641
1721
  const customIds = (await customRoutes()).map((route) => route.id);
1642
- 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"];
1643
1723
  return [.../* @__PURE__ */ new Set([...customIds, ...builtinIds])];
1644
1724
  }
1645
1725
  async function cachedUsageRoute(context, registry) {
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
6
6
 
7
+ const ONE_API_HARD_LIMIT_SENTINEL_USD = 1_000_000;
8
+
7
9
  export function pickNumber(obj, keys) {
8
10
  for (const key of keys) {
9
11
  const value = obj?.[key];
@@ -112,15 +114,34 @@ export async function formatQuota(data) {
112
114
  return keys ? `received (${keys})` : `checked ${unit}`;
113
115
  }
114
116
 
115
- export async function formatNewApiTokenLine(data) {
117
+ export function newApiTokenQuota(data) {
116
118
  const root = usageRoot(data);
117
119
  const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
118
- const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
119
- const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
120
- 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
+ ]);
121
137
  if (remaining === undefined && quota !== undefined && used !== undefined) {
122
138
  remaining = Math.max(0, quota - used);
123
139
  }
140
+ return { unlimited, quota, used, remaining };
141
+ }
142
+
143
+ export async function formatNewApiTokenLine(data) {
144
+ const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
124
145
 
125
146
  if (!unlimited && quota === undefined && used === undefined && remaining === undefined) {
126
147
  throw new Error("NewAPI token usage payload has no quota fields");
@@ -128,7 +149,7 @@ export async function formatNewApiTokenLine(data) {
128
149
 
129
150
  const parts = [];
130
151
  if (unlimited) parts.push("unlimited");
131
- if (remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
152
+ if (!unlimited && remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
132
153
  if (used !== undefined && quota !== undefined) {
133
154
  parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
134
155
  } else if (used !== undefined) {
@@ -158,9 +179,62 @@ export function formatOpenRouterLine(data) {
158
179
  }
159
180
 
160
181
  export function formatOneApiBillingLine(limit, used) {
182
+ if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
161
183
  return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
162
184
  }
163
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(" | ");
236
+ }
237
+
164
238
  function formatQuotaLimitedLine(root) {
165
239
  const quota = root?.quota || {};
166
240
  const limit = pickNumber(quota, ["limit", "quota"]);
@@ -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
 
@@ -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;
@@ -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.
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.8.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": {