@kairyou/agent-tools 0.10.0 → 0.10.1

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
@@ -153,21 +153,23 @@ Output examples:
153
153
 
154
154
  ```text
155
155
  # Plan limits (sub2api / openai-compatible).
156
- D $0.0/$100 | W $0.0/$300 | Exp 07-08
156
+ D $0.0/$100 | W $0.0/$300 ⟳3d1h | Exp 07-08
157
157
 
158
158
  # Multi-window limits (claude-code-hub).
159
159
  5h $2.1/$10.0 | D $8.0/$20.0 | T $19.0/$100 | Exp 08-31
160
160
 
161
161
  # Balance and usage (one-api / one-hub / done-hub / new-api / openrouter).
162
- balance $15.0 | used $5.0/$20.0
162
+ balance $15.0 | used $5.0/$20.0 | ⟳3d1h
163
163
 
164
164
  # Wallet and recent spend (sub2api).
165
165
  balance $362 | today $61.7 | 30d $566
166
166
  ```
167
167
 
168
168
  Fields: `5h/D/W/M/T` are five-hour/daily/weekly/monthly/total spend against
169
- limits; `Exp` is the plan expiry; `balance` is wallet credit; `used` is consumed
170
- credit; `today` and `30d` are today's and the last 30 days' API spend.
169
+ limits; `⟳` is the time until a known limit reset; `Exp` is the plan expiry;
170
+ `balance` is wallet credit; `used` is consumed credit; `today` and `30d` are
171
+ today's and the last 30 days' API spend. A reset countdown is shown only when
172
+ the gateway returns enough timing information to determine it reliably.
171
173
 
172
174
  #### Custom gateway routes
173
175
 
package/README.zh-CN.md CHANGED
@@ -149,20 +149,22 @@ npx -y @kairyou/agent-tools@latest usage -a claude codex opencode
149
149
 
150
150
  ```text
151
151
  # 套餐限额 (sub2api / openai-compatible).
152
- D $0.0/$100 | W $0.0/$300 | Exp 07-08
152
+ D $0.0/$100 | W $0.0/$300 ⟳3d1h | Exp 07-08
153
153
 
154
154
  # 多窗口限额 (claude-code-hub).
155
155
  5h $2.1/$10.0 | D $8.0/$20.0 | T $19.0/$100 | Exp 08-31
156
156
 
157
157
  # 余额与用量 (one-api / one-hub / done-hub / new-api / openrouter).
158
- balance $15.0 | used $5.0/$20.0
158
+ balance $15.0 | used $5.0/$20.0 | ⟳3d1h
159
159
 
160
160
  # 钱包与近期消耗 (sub2api).
161
161
  balance $362 | today $61.7 | 30d $566
162
162
  ```
163
163
 
164
- 字段含义: `5h/D/W/M/T` 是 5 小时/日/周/月/总消耗与上限, `Exp` 是套餐到期日,
165
- `balance` 是钱包余额, `used` 是已用额度, `today` / `30d` 是今日与近 30 天 API 消耗.
164
+ 字段含义: `5h/D/W/M/T` 是 5 小时/日/周/月/总消耗与上限, `⟳` 后面是已知限额的
165
+ 重置倒计时, `Exp` 是套餐到期日, `balance` 是钱包余额, `used` 是已用额度,
166
+ `today` / `30d` 是今日与近 30 天 API 消耗. 只有网关返回的信息足以可靠确定
167
+ 重置时间时才会显示倒计时.
166
168
 
167
169
  #### 自定义网关路由
168
170
 
@@ -1309,6 +1309,14 @@ async function requestJson(url, options = {}) {
1309
1309
 
1310
1310
  // integrations/usage/lib/format.mjs
1311
1311
  var ONE_API_HARD_LIMIT_SENTINEL_USD = 1e6;
1312
+ var RESET_TIME_FIELDS = Object.freeze({
1313
+ // Sub2API rate-limit entries returned alongside quota usage.
1314
+ SUB2API_RATE_LIMIT: "reset_at",
1315
+ // Sub2API /v1/usage response: subscription weekly-window anchor.
1316
+ SUB2API_SUBSCRIPTION_WEEKLY_START: "weekly_window_start",
1317
+ // OpenRouter /api/v1/key response: absolute key-limit reset.
1318
+ OPENROUTER_KEY_LIMIT: "limit_reset"
1319
+ });
1312
1320
  function pickNumber(obj, keys) {
1313
1321
  for (const key of keys) {
1314
1322
  const value = obj?.[key];
@@ -1337,6 +1345,39 @@ function shortDate(value) {
1337
1345
  const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})/);
1338
1346
  return match ? `${match[2]}-${match[3]}` : "";
1339
1347
  }
1348
+ function timestampMs(value) {
1349
+ if (typeof value === "number" && Number.isFinite(value)) {
1350
+ return value < 1e12 ? value * 1e3 : value;
1351
+ }
1352
+ const parsed = Date.parse(value);
1353
+ return Number.isFinite(parsed) ? parsed : void 0;
1354
+ }
1355
+ function compactDurationUntil(value) {
1356
+ const resetMs = timestampMs(value);
1357
+ if (resetMs === void 0) return "";
1358
+ const remainingMs = resetMs - Date.now();
1359
+ if (remainingMs <= 0) return "";
1360
+ let seconds = Math.floor(remainingMs / 1e3);
1361
+ if (seconds <= 0) return "0m";
1362
+ const days = Math.floor(seconds / 86400);
1363
+ seconds %= 86400;
1364
+ const hours = Math.floor(seconds / 3600);
1365
+ seconds %= 3600;
1366
+ const minutes = Math.floor(seconds / 60);
1367
+ if (days) return `${days}d${hours}h`;
1368
+ if (hours) return `${hours}h${minutes}m`;
1369
+ return `${minutes}m`;
1370
+ }
1371
+ function rateLimitResetAt(entry) {
1372
+ const value = entry?.[RESET_TIME_FIELDS.SUB2API_RATE_LIMIT];
1373
+ return timestampMs(value) === void 0 ? void 0 : value;
1374
+ }
1375
+ function weeklyResetAt(subscription) {
1376
+ const startMs = timestampMs(
1377
+ subscription?.[RESET_TIME_FIELDS.SUB2API_SUBSCRIPTION_WEEKLY_START]
1378
+ );
1379
+ return startMs === void 0 ? void 0 : startMs + 7 * 86400 * 1e3;
1380
+ }
1340
1381
  function hasSubscriptionLimits(root) {
1341
1382
  const sub = root?.subscription || {};
1342
1383
  return [
@@ -1437,7 +1478,7 @@ function formatOpenRouterLine(data) {
1437
1478
  const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
1438
1479
  const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
1439
1480
  const used = pickNumber(root, ["usage", "total_usage", "spend"]);
1440
- const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
1481
+ const reset = compactDurationUntil(root?.[RESET_TIME_FIELDS.OPENROUTER_KEY_LIMIT]);
1441
1482
  const parts = [];
1442
1483
  if (remaining !== void 0) parts.push(`balance ${formatMoney(remaining)}`);
1443
1484
  if (used !== void 0 && limit !== void 0 && limit !== remaining) {
@@ -1445,7 +1486,7 @@ function formatOpenRouterLine(data) {
1445
1486
  } else if (used !== void 0) {
1446
1487
  parts.push(`used ${formatMoney(used)}`);
1447
1488
  }
1448
- if (reset) parts.push(`Reset ${reset}`);
1489
+ if (reset) parts.push(`\u27F3${reset}`);
1449
1490
  if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
1450
1491
  return parts.join(" | ");
1451
1492
  }
@@ -1511,7 +1552,8 @@ function formatQuotaLimitedLine(root) {
1511
1552
  const window = entry?.window;
1512
1553
  const rateLimit = pickNumber(entry, ["limit"]);
1513
1554
  const rateUsed = pickNumber(entry, ["used"]);
1514
- return window && rateLimit !== void 0 && rateUsed !== void 0 ? `${window} ${formatMoney(rateUsed)}/${formatMoney(rateLimit)}` : "";
1555
+ const reset = compactDurationUntil(rateLimitResetAt(entry));
1556
+ return window && rateLimit !== void 0 && rateUsed !== void 0 ? `${window} ${formatMoney(rateUsed)}/${formatMoney(rateLimit)}${reset ? ` \u27F3${reset}` : ""}` : "";
1515
1557
  }).filter(Boolean);
1516
1558
  if (rateParts.length > 0) parts.push(rateParts.join(", "));
1517
1559
  }
@@ -1538,9 +1580,12 @@ function formatUsageLine(root) {
1538
1580
  const monthlyLimit = pickNumber(sub, ["monthly_limit_usd"]);
1539
1581
  const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
1540
1582
  const expires = shortDate(sub.expires_at);
1583
+ const weeklyReset = compactDurationUntil(weeklyResetAt(sub));
1541
1584
  const parts = [];
1542
1585
  if (dailyLimit > 0 && dailyUsage !== void 0) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
1543
- if (weeklyLimit > 0 && weeklyUsage !== void 0) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
1586
+ if (weeklyLimit > 0 && weeklyUsage !== void 0) {
1587
+ parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}${weeklyReset ? ` \u27F3${weeklyReset}` : ""}`);
1588
+ }
1544
1589
  if (monthlyLimit > 0 && monthlyUsage !== void 0) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
1545
1590
  if (expires) parts.push(`Exp ${expires}`);
1546
1591
  return parts.join(" | ");
@@ -6,6 +6,15 @@ import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
6
6
 
7
7
  const ONE_API_HARD_LIMIT_SENTINEL_USD = 1_000_000;
8
8
 
9
+ const RESET_TIME_FIELDS = Object.freeze({
10
+ // Sub2API rate-limit entries returned alongside quota usage.
11
+ SUB2API_RATE_LIMIT: "reset_at",
12
+ // Sub2API /v1/usage response: subscription weekly-window anchor.
13
+ SUB2API_SUBSCRIPTION_WEEKLY_START: "weekly_window_start",
14
+ // OpenRouter /api/v1/key response: absolute key-limit reset.
15
+ OPENROUTER_KEY_LIMIT: "limit_reset",
16
+ });
17
+
9
18
  export function pickNumber(obj, keys) {
10
19
  for (const key of keys) {
11
20
  const value = obj?.[key];
@@ -40,6 +49,45 @@ function shortDate(value) {
40
49
  return match ? `${match[2]}-${match[3]}` : "";
41
50
  }
42
51
 
52
+ function timestampMs(value) {
53
+ if (typeof value === "number" && Number.isFinite(value)) {
54
+ return value < 1e12 ? value * 1000 : value;
55
+ }
56
+ const parsed = Date.parse(value);
57
+ return Number.isFinite(parsed) ? parsed : undefined;
58
+ }
59
+
60
+ function compactDurationUntil(value) {
61
+ const resetMs = timestampMs(value);
62
+ if (resetMs === undefined) return "";
63
+
64
+ const remainingMs = resetMs - Date.now();
65
+ if (remainingMs <= 0) return "";
66
+
67
+ let seconds = Math.floor(remainingMs / 1000);
68
+ if (seconds <= 0) return "0m";
69
+ const days = Math.floor(seconds / 86400);
70
+ seconds %= 86400;
71
+ const hours = Math.floor(seconds / 3600);
72
+ seconds %= 3600;
73
+ const minutes = Math.floor(seconds / 60);
74
+ if (days) return `${days}d${hours}h`;
75
+ if (hours) return `${hours}h${minutes}m`;
76
+ return `${minutes}m`;
77
+ }
78
+
79
+ function rateLimitResetAt(entry) {
80
+ const value = entry?.[RESET_TIME_FIELDS.SUB2API_RATE_LIMIT];
81
+ return timestampMs(value) === undefined ? undefined : value;
82
+ }
83
+
84
+ function weeklyResetAt(subscription) {
85
+ const startMs = timestampMs(
86
+ subscription?.[RESET_TIME_FIELDS.SUB2API_SUBSCRIPTION_WEEKLY_START]
87
+ );
88
+ return startMs === undefined ? undefined : startMs + 7 * 86400 * 1000;
89
+ }
90
+
43
91
  function hasSubscriptionLimits(root) {
44
92
  const sub = root?.subscription || {};
45
93
  return [
@@ -163,7 +211,7 @@ export function formatOpenRouterLine(data) {
163
211
  const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
164
212
  const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
165
213
  const used = pickNumber(root, ["usage", "total_usage", "spend"]);
166
- const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
214
+ const reset = compactDurationUntil(root?.[RESET_TIME_FIELDS.OPENROUTER_KEY_LIMIT]);
167
215
  const parts = [];
168
216
 
169
217
  if (remaining !== undefined) parts.push(`balance ${formatMoney(remaining)}`);
@@ -172,7 +220,7 @@ export function formatOpenRouterLine(data) {
172
220
  } else if (used !== undefined) {
173
221
  parts.push(`used ${formatMoney(used)}`);
174
222
  }
175
- if (reset) parts.push(`Reset ${reset}`);
223
+ if (reset) parts.push(`⟳${reset}`);
176
224
 
177
225
  if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
178
226
  return parts.join(" | ");
@@ -254,8 +302,9 @@ function formatQuotaLimitedLine(root) {
254
302
  const window = entry?.window;
255
303
  const rateLimit = pickNumber(entry, ["limit"]);
256
304
  const rateUsed = pickNumber(entry, ["used"]);
305
+ const reset = compactDurationUntil(rateLimitResetAt(entry));
257
306
  return window && rateLimit !== undefined && rateUsed !== undefined
258
- ? `${window} ${formatMoney(rateUsed)}/${formatMoney(rateLimit)}`
307
+ ? `${window} ${formatMoney(rateUsed)}/${formatMoney(rateLimit)}${reset ? ` ⟳${reset}` : ""}`
259
308
  : "";
260
309
  })
261
310
  .filter(Boolean);
@@ -290,10 +339,13 @@ function formatUsageLine(root) {
290
339
  const monthlyLimit = pickNumber(sub, ["monthly_limit_usd"]);
291
340
  const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
292
341
  const expires = shortDate(sub.expires_at);
342
+ const weeklyReset = compactDurationUntil(weeklyResetAt(sub));
293
343
 
294
344
  const parts = [];
295
345
  if (dailyLimit > 0 && dailyUsage !== undefined) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
296
- if (weeklyLimit > 0 && weeklyUsage !== undefined) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
346
+ if (weeklyLimit > 0 && weeklyUsage !== undefined) {
347
+ parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}${weeklyReset ? ` ⟳${weeklyReset}` : ""}`);
348
+ }
297
349
  if (monthlyLimit > 0 && monthlyUsage !== undefined) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
298
350
  if (expires) parts.push(`Exp ${expires}`);
299
351
  return parts.join(" | ");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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": {