@kairyou/agent-tools 0.7.2 → 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 +8 -12
- package/README.zh-CN.md +8 -9
- package/dist/statusline/claude-statusline.mjs +0 -0
- package/dist/usage/cli.mjs +0 -0
- package/dist/usage/codex-hook.mjs +0 -0
- package/dist/usage/core.mjs +24 -27
- package/dist/usage/opencode-plugin.mjs +1 -4
- package/dist/usage/opencode-tui.mjs +1 -4
- package/dist/vision/mcp-server.mjs +0 -0
- package/integrations/usage/lib/format.mjs +23 -25
- package/integrations/usage/lib/routes.mjs +3 -3
- package/integrations/usage/opencode-plugin.mjs +1 -5
- package/integrations/usage/opencode-tui.mjs +1 -5
- package/package.json +7 -2
- package/scripts/install.mjs +111 -21
- package/scripts/publish.mjs +108 -0
- package/scripts/release.mjs +115 -87
package/README.md
CHANGED
|
@@ -75,8 +75,8 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
|
|
|
75
75
|
|
|
76
76
|
`--dry-run` previews, `--uninstall` unwires the integration from the agent, and
|
|
77
77
|
re-running the install command updates. The installer only touches config
|
|
78
|
-
entries it wrote itself
|
|
79
|
-
|
|
78
|
+
entries it wrote itself, and `config.jsonc` updates only add missing default
|
|
79
|
+
keys without touching your edits or comments.
|
|
80
80
|
|
|
81
81
|
| Capability | Claude Code | Codex | OpenCode |
|
|
82
82
|
| --- | --- | --- | --- |
|
|
@@ -90,24 +90,20 @@ uninstall.
|
|
|
90
90
|
npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
The installer writes `statusLine` to `~/.claude/settings.json`.
|
|
94
|
-
output is:
|
|
93
|
+
The installer writes `statusLine` to `~/.claude/settings.json`. Example output:
|
|
95
94
|
|
|
96
95
|
```text
|
|
96
|
+
# Pick and order the fields via statusline.fields in ~/.agent-tools/config.jsonc:
|
|
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 |
|
|
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
|
|
104
104
|
countdown; see [Provider usage](#provider-usage) below for relay quota
|
|
105
105
|
compatibility and configuration.
|
|
106
106
|
|
|
107
|
-
To choose what appears, edit `statusline.fields` in
|
|
108
|
-
`~/.agent-tools/config.jsonc`. Installer updates only add missing default keys
|
|
109
|
-
and never touch your edits or comments.
|
|
110
|
-
|
|
111
107
|
### Provider usage
|
|
112
108
|
|
|
113
109
|
For API relay / gateway setups: shows the relay's balance / quota inside the
|
|
@@ -157,10 +153,10 @@ Output examples:
|
|
|
157
153
|
|
|
158
154
|
```text
|
|
159
155
|
# Relay plan quota.
|
|
160
|
-
|
|
156
|
+
D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
161
157
|
|
|
162
158
|
# Wallet balance.
|
|
163
|
-
|
|
159
|
+
balance $362 | today $61.7 | 30d $566
|
|
164
160
|
```
|
|
165
161
|
|
|
166
162
|
Fields: `D/W/M` are daily/weekly/monthly spend against plan limits; `Exp` is
|
|
@@ -207,7 +203,7 @@ export async function run(context, { requestJson, agentConfig }) {
|
|
|
207
203
|
const me = await requestJson(`${context.baseUrl}/api/user/self`, {
|
|
208
204
|
headers: { authorization: `Bearer ${session?.data?.accessToken}` },
|
|
209
205
|
});
|
|
210
|
-
return { text: `
|
|
206
|
+
return { text: `balance ¥${me?.data?.balance}` };
|
|
211
207
|
}
|
|
212
208
|
```
|
|
213
209
|
|
package/README.zh-CN.md
CHANGED
|
@@ -72,7 +72,8 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
`--dry-run` 预览, `--uninstall` 解除对应集成, 重新执行安装命令即为更新.
|
|
75
|
-
|
|
75
|
+
安装器只改动自己写入的配置项, 更新 `config.jsonc` 时只补充缺失的默认键,
|
|
76
|
+
不会动你的修改和注释.
|
|
76
77
|
|
|
77
78
|
| Capability | Claude Code | Codex | OpenCode |
|
|
78
79
|
| --- | --- | --- | --- |
|
|
@@ -86,21 +87,19 @@ npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
|
|
|
86
87
|
npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
87
88
|
```
|
|
88
89
|
|
|
89
|
-
安装器会把 `statusLine` 写入 `~/.claude/settings.json`.
|
|
90
|
+
安装器会把 `statusLine` 写入 `~/.claude/settings.json`. 输出示例:
|
|
90
91
|
|
|
91
92
|
```text
|
|
93
|
+
# 显示项与顺序由 ~/.agent-tools/config.jsonc 的 statusline.fields 控制:
|
|
92
94
|
⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
|
|
93
95
|
|
|
94
96
|
# 使用兼容的 API 中转时, 显示中转的额度信息, 比如:
|
|
95
|
-
⎇ main | Opus 4.8 |
|
|
97
|
+
⎇ main | Opus 4.8 | balance $362 | today $61.7 | 30d $566
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
其中 `5h` / `w` 是 Claude 的滚动用量窗口, `⟳` 后面是重置倒计时;
|
|
99
101
|
中转额度信息的兼容性与配置见下方 [Provider usage](#provider-usage).
|
|
100
102
|
|
|
101
|
-
如需控制显示项, 修改 `~/.agent-tools/config.jsonc` 里的
|
|
102
|
-
`statusline.fields`. 安装器更新时只会补充缺失的默认键, 不会动你的修改和注释.
|
|
103
|
-
|
|
104
103
|
### Provider usage
|
|
105
104
|
|
|
106
105
|
面向使用 API 中转的场景: 在 agent 内直接显示中转网关的余额/额度, 按量付费或
|
|
@@ -145,10 +144,10 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
145
144
|
|
|
146
145
|
```text
|
|
147
146
|
# 中转套餐额度.
|
|
148
|
-
|
|
147
|
+
D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
149
148
|
|
|
150
149
|
# 钱包余额.
|
|
151
|
-
|
|
150
|
+
balance $362 | today $61.7 | 30d $566
|
|
152
151
|
```
|
|
153
152
|
|
|
154
153
|
字段含义: `D/W/M` 是日/周/月套餐消耗与上限, `Exp` 是套餐到期日,
|
|
@@ -193,7 +192,7 @@ export async function run(context, { requestJson, agentConfig }) {
|
|
|
193
192
|
const me = await requestJson(`${context.baseUrl}/api/user/self`, {
|
|
194
193
|
headers: { authorization: `Bearer ${session?.data?.accessToken}` },
|
|
195
194
|
});
|
|
196
|
-
return { text: `
|
|
195
|
+
return { text: `balance ¥${me?.data?.balance}` };
|
|
197
196
|
}
|
|
198
197
|
```
|
|
199
198
|
|
|
File without changes
|
package/dist/usage/cli.mjs
CHANGED
|
File without changes
|
|
File without changes
|
package/dist/usage/core.mjs
CHANGED
|
@@ -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(
|
|
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(
|
|
1339
|
-
if (isSubscriptionUsage(root)) return formatUsageLine(
|
|
1340
|
-
if (isWalletUsage(root)) return await formatWalletLine(
|
|
1341
|
-
if (remaining !== void 0) return formatUsageLine(
|
|
1342
|
-
if (balance !== void 0) return `
|
|
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 `
|
|
1341
|
+
return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
|
|
1345
1342
|
}
|
|
1346
|
-
if (hardLimit !== void 0) return `
|
|
1343
|
+
if (hardLimit !== void 0) return `total ${formatMaybeMoney(hardLimit, unit)}`;
|
|
1347
1344
|
const keys = Object.keys(root || {}).slice(0, 4).join(", ");
|
|
1348
|
-
return keys ? `
|
|
1345
|
+
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
1349
1346
|
}
|
|
1350
|
-
async function formatNewApiTokenLine(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 ===
|
|
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 `
|
|
1387
|
+
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
1391
1388
|
}
|
|
1392
|
-
function formatQuotaLimitedLine(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
38
|
+
message: text,
|
|
42
39
|
variant: "info",
|
|
43
40
|
duration: 8e3
|
|
44
41
|
}
|
|
@@ -4,9 +4,6 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
|
|
6
6
|
var SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
|
|
7
|
-
function toastMessage(text) {
|
|
8
|
-
return text.replace(/^API \| /, "");
|
|
9
|
-
}
|
|
10
7
|
function latestUsage() {
|
|
11
8
|
try {
|
|
12
9
|
const data = JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8"));
|
|
@@ -28,7 +25,7 @@ var tui = async (api) => {
|
|
|
28
25
|
const message = latestUsage();
|
|
29
26
|
api.ui.toast({
|
|
30
27
|
title: "Provider usage",
|
|
31
|
-
message: message
|
|
28
|
+
message: message || "Provider usage is not available yet",
|
|
32
29
|
variant: message ? "info" : "warning",
|
|
33
30
|
duration: 8e3
|
|
34
31
|
});
|
|
File without changes
|
|
@@ -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(
|
|
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(
|
|
104
|
-
if (isSubscriptionUsage(root)) return formatUsageLine(
|
|
105
|
-
if (isWalletUsage(root)) return await formatWalletLine(
|
|
106
|
-
if (remaining !== undefined) return formatUsageLine(
|
|
107
|
-
if (balance !== undefined) return `
|
|
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 `
|
|
107
|
+
return `remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
|
|
110
108
|
}
|
|
111
|
-
if (hardLimit !== undefined) return `
|
|
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 ? `
|
|
112
|
+
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
export async function formatNewApiTokenLine(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 ===
|
|
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 `
|
|
161
|
+
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
164
162
|
}
|
|
165
163
|
|
|
166
|
-
function formatQuotaLimitedLine(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
48
|
+
message: text,
|
|
53
49
|
variant: "info",
|
|
54
50
|
duration: 8000,
|
|
55
51
|
},
|
|
@@ -5,10 +5,6 @@ import { join } from "node:path";
|
|
|
5
5
|
const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
|
|
6
6
|
const SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
|
|
7
7
|
|
|
8
|
-
function toastMessage(text) {
|
|
9
|
-
return text.replace(/^API \| /, "");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
function latestUsage() {
|
|
13
9
|
try {
|
|
14
10
|
const data = JSON.parse(readFileSync(SNAPSHOT_PATH, "utf8"));
|
|
@@ -33,7 +29,7 @@ const tui = async (api) => {
|
|
|
33
29
|
const message = latestUsage();
|
|
34
30
|
api.ui.toast({
|
|
35
31
|
title: "Provider usage",
|
|
36
|
-
message: message
|
|
32
|
+
message: message || "Provider usage is not available yet",
|
|
37
33
|
variant: message ? "info" : "warning",
|
|
38
34
|
duration: 8000,
|
|
39
35
|
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairyou/agent-tools",
|
|
3
|
-
"version": "0.
|
|
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
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/kairyou/agent-tools.git"
|
|
9
|
+
},
|
|
6
10
|
"engines": {
|
|
7
11
|
"node": ">=22"
|
|
8
12
|
},
|
|
@@ -28,7 +32,8 @@
|
|
|
28
32
|
"build": "node scripts/build.mjs",
|
|
29
33
|
"prepare": "npm run build",
|
|
30
34
|
"test": "node --test tests/*.test.mjs",
|
|
31
|
-
"release": "node scripts/release.mjs"
|
|
35
|
+
"release": "node scripts/release.mjs --target=registry",
|
|
36
|
+
"release:github": "node scripts/release.mjs --target=github"
|
|
32
37
|
},
|
|
33
38
|
"bin": {
|
|
34
39
|
"agent-tools": "./scripts/install.mjs"
|
package/scripts/install.mjs
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
// -h, --help Show this help.
|
|
46
46
|
|
|
47
47
|
import { spawnSync } from "node:child_process";
|
|
48
|
+
import { createHash } from "node:crypto";
|
|
48
49
|
import fs from "node:fs";
|
|
49
50
|
import os from "node:os";
|
|
50
51
|
import path from "node:path";
|
|
@@ -93,6 +94,7 @@ const USAGE_SKILL_NAME = "at-usage";
|
|
|
93
94
|
const USAGE_SKILL_SRC = path.join(REPO_ROOT, "integrations", "usage", "skills", USAGE_SKILL_NAME);
|
|
94
95
|
const VISION_BUNDLED_MCP_SERVER = path.join(REPO_ROOT, "dist", "vision", "mcp-server.mjs");
|
|
95
96
|
const VISION_BUNDLED_CLI = path.join(REPO_ROOT, "dist", "vision", "cli.mjs");
|
|
97
|
+
const INSTALL_STATE_PATH = path.join(INSTALL_ROOT, "install-state.json");
|
|
96
98
|
|
|
97
99
|
function fwd(p) {
|
|
98
100
|
return p.replace(/\\/g, "/");
|
|
@@ -445,13 +447,12 @@ const VISION_RATE_LIMIT_STATE = path.join(INSTALL_ROOT, "cache", "vision-rate-li
|
|
|
445
447
|
const VISION_DIST_DIR = path.join(REPO_ROOT, "dist", "vision");
|
|
446
448
|
const VISION_RUNTIME_SERVER = path.join(VISION_RUNTIME_DIR, "mcp-server.mjs");
|
|
447
449
|
const VISION_RUNTIME_CLI = path.join(VISION_RUNTIME_DIR, "cli.mjs");
|
|
448
|
-
const
|
|
449
|
-
const VISION_SKILL_MARKER_DATA = Object.freeze({
|
|
450
|
+
const VISION_SKILL_IDENTITY = Object.freeze({
|
|
450
451
|
owner: "@kairyou/agent-tools",
|
|
451
452
|
capability: "vision",
|
|
452
453
|
artifact: "skill",
|
|
453
454
|
});
|
|
454
|
-
const
|
|
455
|
+
const USAGE_SKILL_IDENTITY = Object.freeze({
|
|
455
456
|
owner: "@kairyou/agent-tools",
|
|
456
457
|
capability: "usage",
|
|
457
458
|
artifact: "skill",
|
|
@@ -558,26 +559,110 @@ function installVisionRuntime(opts) {
|
|
|
558
559
|
}
|
|
559
560
|
}
|
|
560
561
|
|
|
561
|
-
function
|
|
562
|
-
const
|
|
563
|
-
|
|
562
|
+
function skillManifestKey(dest) {
|
|
563
|
+
const resolved = fwd(path.resolve(dest));
|
|
564
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function skillHash(skillFile) {
|
|
568
|
+
if (!fs.existsSync(skillFile)) return "";
|
|
569
|
+
return createHash("sha256").update(fs.readFileSync(skillFile)).digest("hex");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function matchesIdentity(value, identity) {
|
|
573
|
+
return Object.entries(identity).every(([key, expected]) => value?.[key] === expected);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function readInstallState() {
|
|
577
|
+
const value = readJson(INSTALL_STATE_PATH);
|
|
578
|
+
if (Object.keys(value).length === 0) return { version: 1, artifacts: {} };
|
|
579
|
+
if (
|
|
580
|
+
value.version !== 1 ||
|
|
581
|
+
!value.artifacts ||
|
|
582
|
+
typeof value.artifacts !== "object" ||
|
|
583
|
+
Array.isArray(value.artifacts)
|
|
584
|
+
) {
|
|
585
|
+
throw new Error(`Cannot parse ${INSTALL_STATE_PATH}: unsupported install state schema`);
|
|
586
|
+
}
|
|
587
|
+
return value;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function writeInstallState(state) {
|
|
591
|
+
if (Object.keys(state.artifacts).length === 0) {
|
|
592
|
+
fs.rmSync(INSTALL_STATE_PATH, { force: true });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const text = JSON.stringify(state, null, 2) + "\n";
|
|
596
|
+
const suffix = `${process.pid}-${Date.now()}`;
|
|
597
|
+
const stage = `${INSTALL_STATE_PATH}.stage-${suffix}`;
|
|
598
|
+
const backup = `${INSTALL_STATE_PATH}.backup-${suffix}`;
|
|
599
|
+
let movedCurrent = false;
|
|
600
|
+
fs.mkdirSync(path.dirname(INSTALL_STATE_PATH), { recursive: true });
|
|
564
601
|
try {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
602
|
+
fs.writeFileSync(stage, text);
|
|
603
|
+
if (fs.existsSync(INSTALL_STATE_PATH)) {
|
|
604
|
+
fs.renameSync(INSTALL_STATE_PATH, backup);
|
|
605
|
+
movedCurrent = true;
|
|
606
|
+
}
|
|
607
|
+
fs.renameSync(stage, INSTALL_STATE_PATH);
|
|
608
|
+
fs.rmSync(backup, { force: true });
|
|
609
|
+
} catch (error) {
|
|
610
|
+
fs.rmSync(stage, { force: true });
|
|
611
|
+
if (movedCurrent && !fs.existsSync(INSTALL_STATE_PATH) && fs.existsSync(backup)) {
|
|
612
|
+
fs.renameSync(backup, INSTALL_STATE_PATH);
|
|
613
|
+
}
|
|
614
|
+
throw error;
|
|
615
|
+
} finally {
|
|
616
|
+
fs.rmSync(stage, { force: true });
|
|
617
|
+
if (fs.existsSync(INSTALL_STATE_PATH)) fs.rmSync(backup, { force: true });
|
|
569
618
|
}
|
|
619
|
+
console.log(` wrote ${INSTALL_STATE_PATH}`);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function managedSkillStatus(dest, identity) {
|
|
623
|
+
const entry = readInstallState().artifacts[skillManifestKey(dest)];
|
|
624
|
+
if (entry) {
|
|
625
|
+
if (!matchesIdentity(entry, identity)) return "other";
|
|
626
|
+
const currentHash = skillHash(path.join(dest, "SKILL.md"));
|
|
627
|
+
return currentHash && currentHash === entry.sha256 ? "managed" : "modified";
|
|
628
|
+
}
|
|
629
|
+
return "unmanaged";
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function recordManagedSkill(dest, identity) {
|
|
633
|
+
const state = readInstallState();
|
|
634
|
+
state.artifacts[skillManifestKey(dest)] = {
|
|
635
|
+
path: fwd(path.resolve(dest)),
|
|
636
|
+
...identity,
|
|
637
|
+
sha256: skillHash(path.join(dest, "SKILL.md")),
|
|
638
|
+
};
|
|
639
|
+
writeInstallState(state);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function forgetManagedSkill(dest) {
|
|
643
|
+
const state = readInstallState();
|
|
644
|
+
if (!state.artifacts[skillManifestKey(dest)]) return;
|
|
645
|
+
delete state.artifacts[skillManifestKey(dest)];
|
|
646
|
+
writeInstallState(state);
|
|
570
647
|
}
|
|
571
648
|
|
|
572
649
|
function installManagedSkillDir(
|
|
573
650
|
skillsRoot,
|
|
574
|
-
{ name, source,
|
|
651
|
+
{ name, source, identity, replacements, remove, dryRun }
|
|
575
652
|
) {
|
|
576
653
|
const dest = path.join(skillsRoot, name);
|
|
577
|
-
const
|
|
654
|
+
const status = fs.existsSync(dest) ? managedSkillStatus(dest, identity) : "missing";
|
|
578
655
|
if (remove) {
|
|
579
|
-
if (!fs.existsSync(dest))
|
|
580
|
-
|
|
656
|
+
if (!fs.existsSync(dest)) {
|
|
657
|
+
if (!dryRun) forgetManagedSkill(dest);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (status === "modified") {
|
|
661
|
+
if (!dryRun) forgetManagedSkill(dest);
|
|
662
|
+
console.log(` kept modified skill and released ownership ${dest}`);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (status !== "managed") {
|
|
581
666
|
console.log(` kept unmanaged ${dest}`);
|
|
582
667
|
return;
|
|
583
668
|
}
|
|
@@ -586,10 +671,18 @@ function installManagedSkillDir(
|
|
|
586
671
|
return;
|
|
587
672
|
}
|
|
588
673
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
674
|
+
forgetManagedSkill(dest);
|
|
589
675
|
console.log(` removed ${dest}`);
|
|
590
676
|
return;
|
|
591
677
|
}
|
|
592
|
-
if (
|
|
678
|
+
if (status === "modified") {
|
|
679
|
+
console.error(
|
|
680
|
+
` Refusing to overwrite modified managed skill directory ${dest}. ` +
|
|
681
|
+
`Move or restore it, then re-run the install.`
|
|
682
|
+
);
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
if (fs.existsSync(dest) && status !== "managed") {
|
|
593
686
|
console.error(
|
|
594
687
|
` Refusing to overwrite existing unowned skill directory ${dest}. ` +
|
|
595
688
|
`Move or remove it, then re-run the install.`
|
|
@@ -612,10 +705,7 @@ function installManagedSkillDir(
|
|
|
612
705
|
skill = skill.replaceAll(token, value);
|
|
613
706
|
}
|
|
614
707
|
fs.writeFileSync(skillFile, skill);
|
|
615
|
-
|
|
616
|
-
path.join(dest, SKILL_MARKER),
|
|
617
|
-
JSON.stringify(markerData, null, 2) + "\n"
|
|
618
|
-
);
|
|
708
|
+
recordManagedSkill(dest, identity);
|
|
619
709
|
console.log(` wrote ${dest}`);
|
|
620
710
|
}
|
|
621
711
|
|
|
@@ -623,7 +713,7 @@ function installVisionSkillDir(skillsRoot, { remove, dryRun }) {
|
|
|
623
713
|
installManagedSkillDir(skillsRoot, {
|
|
624
714
|
name: VISION_SKILL_NAME,
|
|
625
715
|
source: VISION_SKILL_SRC,
|
|
626
|
-
|
|
716
|
+
identity: VISION_SKILL_IDENTITY,
|
|
627
717
|
replacements: { "{{VISION_CLI_PATH}}": fwd(VISION_RUNTIME_CLI) },
|
|
628
718
|
remove,
|
|
629
719
|
dryRun,
|
|
@@ -634,7 +724,7 @@ function installUsageSkillDir(skillsRoot, agent, { remove, dryRun }) {
|
|
|
634
724
|
installManagedSkillDir(skillsRoot, {
|
|
635
725
|
name: USAGE_SKILL_NAME,
|
|
636
726
|
source: USAGE_SKILL_SRC,
|
|
637
|
-
|
|
727
|
+
identity: USAGE_SKILL_IDENTITY,
|
|
638
728
|
replacements: {
|
|
639
729
|
"{{USAGE_CLI_PATH}}": fwd(RUNTIME.usageCli),
|
|
640
730
|
"{{USAGE_AGENT}}": agent,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Internal registry publisher used by release.mjs.
|
|
3
|
+
// Publishes to npmjs or an npm-compatible private registry without changing the version.
|
|
4
|
+
// Credentials persist in ~/.agent-tools/publish.npmrc, leaving ~/.npmrc untouched.
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
13
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
|
|
14
|
+
const PACKAGE_NAME = packageJson.name;
|
|
15
|
+
const PUBLISH_NPMRC = path.join(
|
|
16
|
+
process.env.AGENT_TOOLS_HOME || path.join(os.homedir(), ".agent-tools"),
|
|
17
|
+
"publish.npmrc"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
let registryOverride;
|
|
21
|
+
let authType;
|
|
22
|
+
let dryRun = false;
|
|
23
|
+
let authOnly = false;
|
|
24
|
+
for (const arg of process.argv.slice(2)) {
|
|
25
|
+
if (arg === "--dry-run") dryRun = true;
|
|
26
|
+
else if (arg === "--auth-only") authOnly = true;
|
|
27
|
+
else if (arg.startsWith("--registry=")) registryOverride = arg.slice("--registry=".length);
|
|
28
|
+
else if (arg.startsWith("--auth-type=")) authType = arg.slice("--auth-type=".length);
|
|
29
|
+
else {
|
|
30
|
+
console.error(
|
|
31
|
+
`Unknown argument: ${arg} ` +
|
|
32
|
+
`(expected --registry=<url>, --auth-type=<type>, --auth-only, or --dry-run)`
|
|
33
|
+
);
|
|
34
|
+
process.exit(2);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
|
|
39
|
+
console.error("package.json must define a non-empty name before publishing.");
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runNpm(args, { capture = false, allowFailure = false } = {}) {
|
|
44
|
+
const npmExecPath = process.env.npm_execpath;
|
|
45
|
+
const command = npmExecPath ? process.execPath : process.platform === "win32" ? "npm.cmd" : "npm";
|
|
46
|
+
const commandArgs = npmExecPath ? [npmExecPath, ...args] : args;
|
|
47
|
+
const result = spawnSync(command, commandArgs, {
|
|
48
|
+
cwd: ROOT,
|
|
49
|
+
encoding: capture ? "utf8" : undefined,
|
|
50
|
+
stdio: capture ? "pipe" : "inherit",
|
|
51
|
+
});
|
|
52
|
+
if (result.error) throw result.error;
|
|
53
|
+
if (!allowFailure && result.status !== 0) process.exit(result.status ?? 1);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readNpmConfig(key) {
|
|
58
|
+
const result = runNpm(["config", "get", key], { capture: true });
|
|
59
|
+
const value = result.stdout.trim();
|
|
60
|
+
return value && value !== "undefined" && value !== "null" ? value : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const packageScope = PACKAGE_NAME.match(/^(@[^/]+)\//)?.[1];
|
|
64
|
+
const registry =
|
|
65
|
+
registryOverride ||
|
|
66
|
+
packageJson.publishConfig?.registry ||
|
|
67
|
+
(packageScope && readNpmConfig(`${packageScope}:registry`)) ||
|
|
68
|
+
readNpmConfig("registry");
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(registry);
|
|
72
|
+
if (!["http:", "https:"].includes(url.protocol)) throw new Error("unsupported protocol");
|
|
73
|
+
} catch {
|
|
74
|
+
console.error("The resolved npm registry must be a valid HTTP(S) URL.");
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(`Package: ${PACKAGE_NAME}@${packageJson.version}`);
|
|
79
|
+
console.log(`Registry: ${registry}`);
|
|
80
|
+
if (new URL(registry).hostname === "registry.npmjs.org") {
|
|
81
|
+
console.log("Note: npmjs releases normally go through `npm run release:github` (OIDC).");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (dryRun) {
|
|
85
|
+
runNpm(["pack", "--dry-run"]);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fs.mkdirSync(path.dirname(PUBLISH_NPMRC), { recursive: true, mode: 0o700 });
|
|
90
|
+
if (!fs.existsSync(PUBLISH_NPMRC)) fs.writeFileSync(PUBLISH_NPMRC, "", { mode: 0o600 });
|
|
91
|
+
|
|
92
|
+
const npmConfigArgs = [`--registry=${registry}`, `--userconfig=${PUBLISH_NPMRC}`];
|
|
93
|
+
const whoami = runNpm(["whoami", ...npmConfigArgs], { capture: true, allowFailure: true });
|
|
94
|
+
if (whoami.status !== 0) {
|
|
95
|
+
console.log(`Authentication required. Credentials will be saved to ${PUBLISH_NPMRC}`);
|
|
96
|
+
const loginArgs = ["login", ...npmConfigArgs];
|
|
97
|
+
if (authType) loginArgs.push(`--auth-type=${authType}`);
|
|
98
|
+
runNpm(loginArgs);
|
|
99
|
+
runNpm(["whoami", ...npmConfigArgs]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (authOnly) {
|
|
103
|
+
console.log(`Authenticated for ${registry}`);
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
runNpm(["publish", ...npmConfigArgs]);
|
|
108
|
+
console.log(`\nPublished ${PACKAGE_NAME}@${packageJson.version} to ${registry}`);
|
package/scripts/release.mjs
CHANGED
|
@@ -2,125 +2,153 @@
|
|
|
2
2
|
// Release helper for this package.
|
|
3
3
|
//
|
|
4
4
|
// Usage:
|
|
5
|
-
// npm run release [-- minor|major]
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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.
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
|
-
import
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
16
18
|
|
|
19
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
20
|
+
const PUBLISH_SCRIPT = path.join(ROOT, "scripts", "publish.mjs");
|
|
17
21
|
const BUMPS = ["patch", "minor", "major"];
|
|
18
|
-
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
|
-
const PACKAGE_NAME = packageJson.name;
|
|
20
|
-
|
|
21
|
-
if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
|
|
22
|
-
console.error("package.json must define a non-empty name before releasing.");
|
|
23
|
-
process.exit(2);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function readNpmConfig(key) {
|
|
27
|
-
const npmExecPath = process.env.npm_execpath;
|
|
28
|
-
const result = npmExecPath
|
|
29
|
-
? spawnSync(process.execPath, [npmExecPath, "config", "get", key], { encoding: "utf8" })
|
|
30
|
-
: spawnSync("npm", ["config", "get", key], { encoding: "utf8", shell: true });
|
|
31
|
-
|
|
32
|
-
if (result.status !== 0) {
|
|
33
|
-
const detail = result.stderr?.trim() || result.error?.message || "npm config exited unexpectedly";
|
|
34
|
-
console.error(`Unable to read npm config ${key}:\n${detail}`);
|
|
35
|
-
process.exit(result.status ?? 2);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const value = result.stdout?.trim();
|
|
39
|
-
return value && value !== "undefined" && value !== "null" ? value : undefined;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const packageScope = PACKAGE_NAME.match(/^(@[^/]+)\//)?.[1];
|
|
43
|
-
const REGISTRY = (packageScope && readNpmConfig(`${packageScope}:registry`)) || readNpmConfig("registry");
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const registryUrl = new URL(REGISTRY);
|
|
47
|
-
if (!["http:", "https:"].includes(registryUrl.protocol)) throw new Error("unsupported protocol");
|
|
48
|
-
} catch {
|
|
49
|
-
console.error("npm registry config must be a valid HTTP(S) URL before releasing.");
|
|
50
|
-
process.exit(2);
|
|
51
|
-
}
|
|
52
22
|
|
|
23
|
+
let target = "registry";
|
|
53
24
|
let bump = "patch";
|
|
25
|
+
let bumpSpecified = false;
|
|
54
26
|
let dryRun = false;
|
|
27
|
+
let publishOnly = false;
|
|
28
|
+
const publishArgs = [];
|
|
55
29
|
for (const arg of process.argv.slice(2)) {
|
|
56
30
|
if (arg === "--dry-run") dryRun = true;
|
|
57
|
-
else if (
|
|
58
|
-
else
|
|
59
|
-
|
|
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
|
+
);
|
|
60
44
|
process.exit(2);
|
|
61
45
|
}
|
|
62
46
|
}
|
|
63
47
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
73
|
if (result.status !== 0) {
|
|
69
|
-
console.error(`\nAborted:
|
|
74
|
+
console.error(`\nAborted: command exited with ${result.status}.`);
|
|
70
75
|
onFailure?.();
|
|
71
76
|
process.exit(result.status ?? 1);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
function
|
|
76
|
-
|
|
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:");
|
|
77
91
|
for (const command of commands) console.error(` ${command}`);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
const status = spawnSync("git status --porcelain", {
|
|
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
|
+
}
|
|
81
99
|
if (!dryRun && status.stdout.trim() !== "") {
|
|
82
|
-
console.error("Working tree is not clean
|
|
100
|
+
console.error("Working tree is not clean - commit or stash first:\n" + status.stdout);
|
|
83
101
|
process.exit(1);
|
|
84
102
|
}
|
|
85
103
|
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
onFailure() {
|
|
90
|
-
printCommands("npm authentication is missing or expired. Log in, verify the account, then retry:", [
|
|
91
|
-
`npm login --registry=${REGISTRY}`,
|
|
92
|
-
`npm whoami --registry=${REGISTRY}`,
|
|
93
|
-
`npm run release -- ${bump}`,
|
|
94
|
-
]);
|
|
95
|
-
},
|
|
104
|
+
if (publishOnly) {
|
|
105
|
+
run(process.execPath, [PUBLISH_SCRIPT, ...publishArgs], {
|
|
106
|
+
label: `node scripts/publish.mjs ${publishArgs.join(" ")}`.trimEnd(),
|
|
96
107
|
});
|
|
108
|
+
process.exit(0);
|
|
97
109
|
}
|
|
98
110
|
|
|
99
|
-
|
|
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"]);
|
|
100
117
|
|
|
101
118
|
if (dryRun) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
console.log("\nDry run complete. No version bump, nothing published.");
|
|
119
|
+
runNpm(["pack", "--dry-run"]);
|
|
120
|
+
console.log("\nDry run complete. No authentication, version bump, publish, or push.");
|
|
105
121
|
process.exit(0);
|
|
106
122
|
}
|
|
107
123
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
[
|
|
114
|
-
`npm login --registry=${REGISTRY}`,
|
|
115
|
-
`npm whoami --registry=${REGISTRY}`,
|
|
116
|
-
`npm owner ls ${PACKAGE_NAME}`,
|
|
117
|
-
"npm publish",
|
|
118
|
-
`npm view ${PACKAGE_NAME} version`,
|
|
119
|
-
"git push --follow-tags",
|
|
120
|
-
],
|
|
121
|
-
);
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
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}`;
|
|
125
129
|
|
|
126
|
-
|
|
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
|
+
}
|