@kairyou/agent-tools 0.7.4 → 0.8.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
@@ -97,7 +97,7 @@ The installer writes `statusLine` to `~/.claude/settings.json`. Example output:
97
97
  ⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
98
98
 
99
99
  # 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
100
+ ⎇ main | Opus 4.8 | balance $362 | today $61.7 | 30d $566
101
101
  ```
102
102
 
103
103
  Here `5h` and `w` are Claude's rolling usage windows and `⟳` is the reset
@@ -153,10 +153,10 @@ Output examples:
153
153
 
154
154
  ```text
155
155
  # Relay plan quota.
156
- API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
156
+ D $0.0/$100 | W $0.0/$300 | Exp 07-08
157
157
 
158
158
  # Wallet balance.
159
- API | balance $362 | today $61.7 | 30d $566
159
+ balance $362 | today $61.7 | 30d $566
160
160
  ```
161
161
 
162
162
  Fields: `D/W/M` are daily/weekly/monthly spend against plan limits; `Exp` is
@@ -203,7 +203,7 @@ export async function run(context, { requestJson, agentConfig }) {
203
203
  const me = await requestJson(`${context.baseUrl}/api/user/self`, {
204
204
  headers: { authorization: `Bearer ${session?.data?.accessToken}` },
205
205
  });
206
- return { text: `API | balance ¥${me?.data?.balance}` };
206
+ return { text: `balance ¥${me?.data?.balance}` };
207
207
  }
208
208
  ```
209
209
 
package/README.zh-CN.md CHANGED
@@ -94,7 +94,7 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
94
94
  ⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
95
95
 
96
96
  # 使用兼容的 API 中转时, 显示中转的额度信息, 比如:
97
- ⎇ main | Opus 4.8 | API | balance $362 | today $61.7 | 30d $566
97
+ ⎇ main | Opus 4.8 | balance $362 | today $61.7 | 30d $566
98
98
  ```
99
99
 
100
100
  其中 `5h` / `w` 是 Claude 的滚动用量窗口, `⟳` 后面是重置倒计时;
@@ -144,10 +144,10 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
144
144
 
145
145
  ```text
146
146
  # 中转套餐额度.
147
- API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
147
+ D $0.0/$100 | W $0.0/$300 | Exp 07-08
148
148
 
149
149
  # 钱包余额.
150
- API | balance $362 | today $61.7 | 30d $566
150
+ balance $362 | today $61.7 | 30d $566
151
151
  ```
152
152
 
153
153
  字段含义: `D/W/M` 是日/周/月套餐消耗与上限, `Exp` 是套餐到期日,
@@ -192,7 +192,7 @@ export async function run(context, { requestJson, agentConfig }) {
192
192
  const me = await requestJson(`${context.baseUrl}/api/user/self`, {
193
193
  headers: { authorization: `Bearer ${session?.data?.accessToken}` },
194
194
  });
195
- return { text: `API | balance ¥${me?.data?.balance}` };
195
+ return { text: `balance ¥${me?.data?.balance}` };
196
196
  }
197
197
  ```
198
198
 
@@ -1276,9 +1276,6 @@ function formatMaybeMoney(value, unit = "USD") {
1276
1276
  if (unit === "USD" || unit === "$") return formatMoney(value);
1277
1277
  return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
1278
1278
  }
1279
- function usageParts() {
1280
- return ["API"];
1281
- }
1282
1279
  async function formatNewApiQuota(value) {
1283
1280
  const scale = await newApiQuotaScale();
1284
1281
  if (Number.isFinite(scale) && scale > 0) return formatMoney(value / scale);
@@ -1328,26 +1325,26 @@ function hasV1UsageFields(root) {
1328
1325
  "usage"
1329
1326
  ]) !== 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
1327
  }
1331
- async function formatQuota(label, data) {
1328
+ async function formatQuota(data) {
1332
1329
  const root = usageRoot(data);
1333
1330
  const unit = root?.unit || "USD";
1334
1331
  const remaining = pickNumber(root, ["remaining"]);
1335
1332
  const hardLimit = pickNumber(root, ["hard_limit_usd", "hard_limit", "total_granted", "quota"]);
1336
1333
  const used = pickNumber(root, ["total_usage", "used", "usage"]);
1337
1334
  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)}`;
1335
+ if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(root);
1336
+ if (isSubscriptionUsage(root)) return formatUsageLine(root);
1337
+ if (isWalletUsage(root)) return await formatWalletLine(root);
1338
+ if (remaining !== void 0) return formatUsageLine(root);
1339
+ if (balance !== void 0) return `balance ${formatMaybeMoney(balance, unit)}`;
1343
1340
  if (hardLimit !== void 0 && used !== void 0) {
1344
- return `API | remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
1341
+ return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
1345
1342
  }
1346
- if (hardLimit !== void 0) return `API | total ${formatMaybeMoney(hardLimit, unit)}`;
1343
+ if (hardLimit !== void 0) return `total ${formatMaybeMoney(hardLimit, unit)}`;
1347
1344
  const keys = Object.keys(root || {}).slice(0, 4).join(", ");
1348
- return keys ? `API | received (${keys})` : `API | checked ${unit}`;
1345
+ return keys ? `received (${keys})` : `checked ${unit}`;
1349
1346
  }
1350
- async function formatNewApiTokenLine(label, data) {
1347
+ async function formatNewApiTokenLine(data) {
1351
1348
  const root = usageRoot(data);
1352
1349
  const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
1353
1350
  const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
@@ -1359,7 +1356,7 @@ async function formatNewApiTokenLine(label, data) {
1359
1356
  if (!unlimited && quota === void 0 && used === void 0 && remaining === void 0) {
1360
1357
  throw new Error("NewAPI token usage payload has no quota fields");
1361
1358
  }
1362
- const parts = usageParts();
1359
+ const parts = [];
1363
1360
  if (unlimited) parts.push("unlimited");
1364
1361
  if (remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
1365
1362
  if (used !== void 0 && quota !== void 0) {
@@ -1369,13 +1366,13 @@ async function formatNewApiTokenLine(label, data) {
1369
1366
  }
1370
1367
  return parts.join(" | ");
1371
1368
  }
1372
- function formatOpenRouterLine(label, data) {
1369
+ function formatOpenRouterLine(data) {
1373
1370
  const root = usageRoot(data);
1374
1371
  const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
1375
1372
  const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
1376
1373
  const used = pickNumber(root, ["usage", "total_usage", "spend"]);
1377
1374
  const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
1378
- const parts = usageParts();
1375
+ const parts = [];
1379
1376
  if (remaining !== void 0) parts.push(`balance ${formatMoney(remaining)}`);
1380
1377
  if (used !== void 0 && limit !== void 0 && limit !== remaining) {
1381
1378
  parts.push(`used ${formatMoney(used)}/${formatMoney(limit)}`);
@@ -1383,18 +1380,18 @@ function formatOpenRouterLine(label, data) {
1383
1380
  parts.push(`used ${formatMoney(used)}`);
1384
1381
  }
1385
1382
  if (reset) parts.push(`Reset ${reset}`);
1386
- if (parts.length === 1) throw new Error("OpenRouter payload has no usage fields");
1383
+ if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
1387
1384
  return parts.join(" | ");
1388
1385
  }
1389
1386
  function formatOneApiBillingLine(limit, used) {
1390
- return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
1387
+ return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
1391
1388
  }
1392
- function formatQuotaLimitedLine(label, root) {
1389
+ function formatQuotaLimitedLine(root) {
1393
1390
  const quota = root?.quota || {};
1394
1391
  const limit = pickNumber(quota, ["limit", "quota"]);
1395
1392
  const used = pickNumber(quota, ["used", "quota_used"]);
1396
1393
  const remaining = pickNumber(quota, ["remaining"]) ?? pickNumber(root, ["remaining"]);
1397
- const parts = usageParts();
1394
+ const parts = [];
1398
1395
  if (limit !== void 0 && used !== void 0) {
1399
1396
  parts.push(`Q ${formatMoney(used)}/${formatMoney(limit)}`);
1400
1397
  } else if (remaining !== void 0) {
@@ -1411,11 +1408,11 @@ function formatQuotaLimitedLine(label, root) {
1411
1408
  }
1412
1409
  return parts.join(" | ");
1413
1410
  }
1414
- async function formatWalletLine(label, root) {
1411
+ async function formatWalletLine(root) {
1415
1412
  const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
1416
1413
  const todayCost = pickNumber(root?.usage?.today, ["actual_cost", "cost"]);
1417
1414
  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();
1415
+ const parts = [];
1419
1416
  if (balance !== void 0) parts.push(`balance ${formatMoney(balance)}`);
1420
1417
  if (todayCost !== void 0) parts.push(`today ${formatMoney(todayCost)}`);
1421
1418
  if (recentUsage !== void 0 && root.daily_usage.length > 0) {
@@ -1423,7 +1420,7 @@ async function formatWalletLine(label, root) {
1423
1420
  }
1424
1421
  return parts.join(" | ");
1425
1422
  }
1426
- function formatUsageLine(label, root) {
1423
+ function formatUsageLine(root) {
1427
1424
  const sub = root?.subscription || {};
1428
1425
  const dailyLimit = pickNumber(sub, ["daily_limit_usd"]);
1429
1426
  const dailyUsage = pickNumber(sub, ["daily_usage_usd"]);
@@ -1432,7 +1429,7 @@ function formatUsageLine(label, root) {
1432
1429
  const monthlyLimit = pickNumber(sub, ["monthly_limit_usd"]);
1433
1430
  const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
1434
1431
  const expires = shortDate(sub.expires_at);
1435
- const parts = usageParts();
1432
+ const parts = [];
1436
1433
  if (dailyLimit > 0 && dailyUsage !== void 0) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
1437
1434
  if (weeklyLimit > 0 && weeklyUsage !== void 0) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
1438
1435
  if (monthlyLimit > 0 && monthlyUsage !== void 0) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
@@ -1462,7 +1459,7 @@ async function fetchV1Usage(context) {
1462
1459
  name: "v1 usage"
1463
1460
  });
1464
1461
  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);
1462
+ return usageResult(context, "v1-usage", await formatQuota(json), json);
1466
1463
  }
1467
1464
  async function fetchNewApiTokenUsage(context) {
1468
1465
  const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/usage/token/"), {
@@ -1491,7 +1488,7 @@ async function fetchNewApiTokenUsage(context) {
1491
1488
  source: "newapi-token",
1492
1489
  raw: json
1493
1490
  };
1494
- return usageResult(context, "newapi-token", await formatNewApiTokenLine(context.label, json), normalized);
1491
+ return usageResult(context, "newapi-token", await formatNewApiTokenLine(json), normalized);
1495
1492
  }
1496
1493
  async function fetchOneApiBillingUsage(context) {
1497
1494
  const base = serviceRoot(context.baseUrl);
@@ -1537,7 +1534,7 @@ async function fetchOpenRouterUsage(context) {
1537
1534
  for (const endpoint of endpoints) {
1538
1535
  try {
1539
1536
  const json = await requestJson(endpoint.url, { key: context.key, name: endpoint.source });
1540
- return usageResult(context, endpoint.source, formatOpenRouterLine("OpenRouter", json), json);
1537
+ return usageResult(context, endpoint.source, formatOpenRouterLine(json), json);
1541
1538
  } catch (error) {
1542
1539
  lastError = error;
1543
1540
  await debugLog({ source: endpoint.source, error: error.message });
@@ -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,4 +1,6 @@
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
 
@@ -20,10 +22,6 @@ function formatMaybeMoney(value, unit = "USD") {
20
22
  return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
21
23
  }
22
24
 
23
- function usageParts() {
24
- return ["API"];
25
- }
26
-
27
25
  async function formatNewApiQuota(value) {
28
26
  const scale = await newApiQuotaScale();
29
27
  if (Number.isFinite(scale) && scale > 0) return formatMoney(value / scale);
@@ -92,7 +90,7 @@ export function hasV1UsageFields(root) {
92
90
  );
93
91
  }
94
92
 
95
- export async function formatQuota(label, data) {
93
+ export async function formatQuota(data) {
96
94
  const root = usageRoot(data);
97
95
  const unit = root?.unit || "USD";
98
96
  const remaining = pickNumber(root, ["remaining"]);
@@ -100,21 +98,21 @@ export async function formatQuota(label, data) {
100
98
  const used = pickNumber(root, ["total_usage", "used", "usage"]);
101
99
  const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
102
100
 
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)}`;
101
+ if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(root);
102
+ if (isSubscriptionUsage(root)) return formatUsageLine(root);
103
+ if (isWalletUsage(root)) return await formatWalletLine(root);
104
+ if (remaining !== undefined) return formatUsageLine(root);
105
+ if (balance !== undefined) return `balance ${formatMaybeMoney(balance, unit)}`;
108
106
  if (hardLimit !== undefined && used !== undefined) {
109
- return `API | remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
107
+ return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
110
108
  }
111
- if (hardLimit !== undefined) return `API | total ${formatMaybeMoney(hardLimit, unit)}`;
109
+ if (hardLimit !== undefined) return `total ${formatMaybeMoney(hardLimit, unit)}`;
112
110
 
113
111
  const keys = Object.keys(root || {}).slice(0, 4).join(", ");
114
- return keys ? `API | received (${keys})` : `API | checked ${unit}`;
112
+ return keys ? `received (${keys})` : `checked ${unit}`;
115
113
  }
116
114
 
117
- export async function formatNewApiTokenLine(label, data) {
115
+ export async function formatNewApiTokenLine(data) {
118
116
  const root = usageRoot(data);
119
117
  const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
120
118
  const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
@@ -128,7 +126,7 @@ export async function formatNewApiTokenLine(label, data) {
128
126
  throw new Error("NewAPI token usage payload has no quota fields");
129
127
  }
130
128
 
131
- const parts = usageParts();
129
+ const parts = [];
132
130
  if (unlimited) parts.push("unlimited");
133
131
  if (remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
134
132
  if (used !== undefined && quota !== undefined) {
@@ -139,13 +137,13 @@ export async function formatNewApiTokenLine(label, data) {
139
137
  return parts.join(" | ");
140
138
  }
141
139
 
142
- export function formatOpenRouterLine(label, data) {
140
+ export function formatOpenRouterLine(data) {
143
141
  const root = usageRoot(data);
144
142
  const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
145
143
  const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
146
144
  const used = pickNumber(root, ["usage", "total_usage", "spend"]);
147
145
  const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
148
- const parts = usageParts();
146
+ const parts = [];
149
147
 
150
148
  if (remaining !== undefined) parts.push(`balance ${formatMoney(remaining)}`);
151
149
  if (used !== undefined && limit !== undefined && limit !== remaining) {
@@ -155,20 +153,20 @@ export function formatOpenRouterLine(label, data) {
155
153
  }
156
154
  if (reset) parts.push(`Reset ${reset}`);
157
155
 
158
- if (parts.length === 1) throw new Error("OpenRouter payload has no usage fields");
156
+ if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
159
157
  return parts.join(" | ");
160
158
  }
161
159
 
162
160
  export function formatOneApiBillingLine(limit, used) {
163
- return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
161
+ return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
164
162
  }
165
163
 
166
- function formatQuotaLimitedLine(label, root) {
164
+ function formatQuotaLimitedLine(root) {
167
165
  const quota = root?.quota || {};
168
166
  const limit = pickNumber(quota, ["limit", "quota"]);
169
167
  const used = pickNumber(quota, ["used", "quota_used"]);
170
168
  const remaining = pickNumber(quota, ["remaining"]) ?? pickNumber(root, ["remaining"]);
171
- const parts = usageParts();
169
+ const parts = [];
172
170
 
173
171
  if (limit !== undefined && used !== undefined) {
174
172
  parts.push(`Q ${formatMoney(used)}/${formatMoney(limit)}`);
@@ -193,14 +191,14 @@ function formatQuotaLimitedLine(label, root) {
193
191
  return parts.join(" | ");
194
192
  }
195
193
 
196
- async function formatWalletLine(label, root) {
194
+ async function formatWalletLine(root) {
197
195
  const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
198
196
  const todayCost = pickNumber(root?.usage?.today, ["actual_cost", "cost"]);
199
197
  const recentUsage = Array.isArray(root?.daily_usage)
200
198
  ? root.daily_usage.reduce((sum, day) => sum + (pickNumber(day, ["actual_cost", "cost"]) || 0), 0)
201
199
  : undefined;
202
200
 
203
- const parts = usageParts();
201
+ const parts = [];
204
202
  if (balance !== undefined) parts.push(`balance ${formatMoney(balance)}`);
205
203
  if (todayCost !== undefined) parts.push(`today ${formatMoney(todayCost)}`);
206
204
  if (recentUsage !== undefined && root.daily_usage.length > 0) {
@@ -209,7 +207,7 @@ async function formatWalletLine(label, root) {
209
207
  return parts.join(" | ");
210
208
  }
211
209
 
212
- function formatUsageLine(label, root) {
210
+ function formatUsageLine(root) {
213
211
  const sub = root?.subscription || {};
214
212
  const dailyLimit = pickNumber(sub, ["daily_limit_usd"]);
215
213
  const dailyUsage = pickNumber(sub, ["daily_usage_usd"]);
@@ -219,7 +217,7 @@ function formatUsageLine(label, root) {
219
217
  const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
220
218
  const expires = shortDate(sub.expires_at);
221
219
 
222
- const parts = usageParts();
220
+ const parts = [];
223
221
  if (dailyLimit > 0 && dailyUsage !== undefined) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
224
222
  if (weeklyLimit > 0 && weeklyUsage !== undefined) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
225
223
  if (monthlyLimit > 0 && monthlyUsage !== undefined) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
@@ -56,7 +56,7 @@ async function fetchV1Usage(context) {
56
56
  name: "v1 usage",
57
57
  });
58
58
  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);
59
+ return usageResult(context, "v1-usage", await formatQuota(json), json);
60
60
  }
61
61
 
62
62
  // New API exposes a read-only usage endpoint authenticated by the same relay
@@ -88,7 +88,7 @@ async function fetchNewApiTokenUsage(context) {
88
88
  source: "newapi-token",
89
89
  raw: json,
90
90
  };
91
- return usageResult(context, "newapi-token", await formatNewApiTokenLine(context.label, json), normalized);
91
+ return usageResult(context, "newapi-token", await formatNewApiTokenLine(json), normalized);
92
92
  }
93
93
 
94
94
  // One API's legacy OpenAI billing endpoints use the same relay API key as
@@ -143,7 +143,7 @@ async function fetchOpenRouterUsage(context) {
143
143
  for (const endpoint of endpoints) {
144
144
  try {
145
145
  const json = await requestJson(endpoint.url, { key: context.key, name: endpoint.source });
146
- return usageResult(context, endpoint.source, formatOpenRouterLine("OpenRouter", json), json);
146
+ return usageResult(context, endpoint.source, formatOpenRouterLine(json), json);
147
147
  } catch (error) {
148
148
  lastError = error;
149
149
  await debugLog({ source: endpoint.source, error: error.message });
@@ -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.8.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
+ }