@kairyou/agent-tools 0.8.0 → 0.10.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 +57 -113
- package/README.zh-CN.md +55 -103
- package/dist/usage/cli.mjs +5 -3
- package/dist/usage/codex-hook.mjs +18 -10
- package/dist/usage/core.mjs +275 -114
- package/docs/en/custom-gateway-routes.md +53 -0
- package/docs/en/repository-structure.md +20 -0
- package/docs/zh-CN/custom-gateway-routes.md +47 -0
- package/docs/zh-CN/repository-structure.md +20 -0
- package/integrations/usage/codex-hook.mjs +19 -10
- package/integrations/usage/core.mjs +60 -12
- package/integrations/usage/lib/cache.mjs +152 -70
- package/integrations/usage/lib/config.mjs +11 -10
- package/integrations/usage/lib/format.mjs +79 -5
- package/integrations/usage/lib/http.mjs +5 -2
- package/integrations/usage/lib/routes.mjs +42 -15
- package/package.json +2 -1
- package/scripts/install.mjs +13 -4
- package/scripts/publish.mjs +6 -2
- package/scripts/release.mjs +8 -6
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
|
|
6
6
|
|
|
7
|
+
const ONE_API_HARD_LIMIT_SENTINEL_USD = 1_000_000;
|
|
8
|
+
|
|
7
9
|
export function pickNumber(obj, keys) {
|
|
8
10
|
for (const key of keys) {
|
|
9
11
|
const value = obj?.[key];
|
|
@@ -112,15 +114,34 @@ export async function formatQuota(data) {
|
|
|
112
114
|
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
export
|
|
117
|
+
export function newApiTokenQuota(data) {
|
|
116
118
|
const root = usageRoot(data);
|
|
117
119
|
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
118
|
-
const quota = pickNumber(root, [
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const quota = pickNumber(root, [
|
|
121
|
+
"quota",
|
|
122
|
+
"limit",
|
|
123
|
+
"total_quota",
|
|
124
|
+
"totalQuota",
|
|
125
|
+
"total_granted",
|
|
126
|
+
"totalGranted",
|
|
127
|
+
]);
|
|
128
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used", "total_used", "totalUsed"]);
|
|
129
|
+
let remaining = pickNumber(root, [
|
|
130
|
+
"remain_quota",
|
|
131
|
+
"remainQuota",
|
|
132
|
+
"remaining",
|
|
133
|
+
"balance",
|
|
134
|
+
"total_available",
|
|
135
|
+
"totalAvailable",
|
|
136
|
+
]);
|
|
121
137
|
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
122
138
|
remaining = Math.max(0, quota - used);
|
|
123
139
|
}
|
|
140
|
+
return { unlimited, quota, used, remaining };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function formatNewApiTokenLine(data) {
|
|
144
|
+
const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
|
|
124
145
|
|
|
125
146
|
if (!unlimited && quota === undefined && used === undefined && remaining === undefined) {
|
|
126
147
|
throw new Error("NewAPI token usage payload has no quota fields");
|
|
@@ -128,7 +149,7 @@ export async function formatNewApiTokenLine(data) {
|
|
|
128
149
|
|
|
129
150
|
const parts = [];
|
|
130
151
|
if (unlimited) parts.push("unlimited");
|
|
131
|
-
if (remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
152
|
+
if (!unlimited && remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
132
153
|
if (used !== undefined && quota !== undefined) {
|
|
133
154
|
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
134
155
|
} else if (used !== undefined) {
|
|
@@ -158,9 +179,62 @@ export function formatOpenRouterLine(data) {
|
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
export function formatOneApiBillingLine(limit, used) {
|
|
182
|
+
if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
|
|
161
183
|
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
162
184
|
}
|
|
163
185
|
|
|
186
|
+
export function hasSpendableOneApiLimit(limit) {
|
|
187
|
+
return Number.isFinite(limit) && limit >= 0 && limit < ONE_API_HARD_LIMIT_SENTINEL_USD;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function effectiveClaudeCodeHubWindow(root, suffix) {
|
|
191
|
+
const keyLimit = pickNumber(root, [`keyLimit${suffix}Usd`]);
|
|
192
|
+
if (keyLimit !== undefined && keyLimit > 0) {
|
|
193
|
+
return { limit: keyLimit, used: pickNumber(root, [`keyCurrent${suffix}Usd`]) };
|
|
194
|
+
}
|
|
195
|
+
const userLimit = pickNumber(root, [`userLimit${suffix}Usd`]);
|
|
196
|
+
return {
|
|
197
|
+
limit: userLimit !== undefined && userLimit > 0 ? userLimit : undefined,
|
|
198
|
+
used: pickNumber(root, [`userCurrent${suffix}Usd`]),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function earliestDate(...values) {
|
|
203
|
+
return values
|
|
204
|
+
.map((value) => ({ value, time: Date.parse(value) }))
|
|
205
|
+
.filter((entry) => entry.value && Number.isFinite(entry.time))
|
|
206
|
+
.sort((left, right) => left.time - right.time)[0]?.value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function formatClaudeCodeHubLine(data) {
|
|
210
|
+
const root = usageRoot(data);
|
|
211
|
+
const windows = [
|
|
212
|
+
["5h", "5h"],
|
|
213
|
+
["D", "Daily"],
|
|
214
|
+
["W", "Weekly"],
|
|
215
|
+
["M", "Monthly"],
|
|
216
|
+
["T", "Total"],
|
|
217
|
+
];
|
|
218
|
+
const parts = [];
|
|
219
|
+
|
|
220
|
+
for (const [label, suffix] of windows) {
|
|
221
|
+
const { limit, used } = effectiveClaudeCodeHubWindow(root, suffix);
|
|
222
|
+
if (limit !== undefined && used !== undefined) {
|
|
223
|
+
parts.push(`${label} ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (parts.length === 0) {
|
|
228
|
+
const totalUsed = pickNumber(root, ["keyCurrentTotalUsd", "userCurrentTotalUsd"]);
|
|
229
|
+
if (totalUsed !== undefined) parts.push(`used ${formatMoney(totalUsed)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const expires = shortDate(earliestDate(root?.expiresAt, root?.userExpiresAt));
|
|
233
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
234
|
+
if (parts.length === 0) throw new Error("Claude Code Hub quota payload has no quota fields");
|
|
235
|
+
return parts.join(" | ");
|
|
236
|
+
}
|
|
237
|
+
|
|
164
238
|
function formatQuotaLimitedLine(root) {
|
|
165
239
|
const quota = root?.quota || {};
|
|
166
240
|
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
import { createContext, runInContext } from "node:vm";
|
|
5
5
|
import { debugLog } from "./config.mjs";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// No user waits on this request: the hook reads a snapshot and refreshes in a
|
|
8
|
+
// detached process, and skill/CLI queries are explicit. Custom routes can pass
|
|
9
|
+
// their own timeoutMs.
|
|
10
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;
|
|
8
11
|
const SHIELD_USER_AGENT =
|
|
9
12
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
|
10
13
|
"(KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
|
|
@@ -126,7 +129,7 @@ function mergeSetCookiePairs(cookieHeader, setCookieHeaders) {
|
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
export async function requestJson(url, options = {}) {
|
|
129
|
-
const { key = "", headers = {}, name = "usage", timeoutMs =
|
|
132
|
+
const { key = "", headers = {}, name = "usage", timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = options;
|
|
130
133
|
let cookieHeader = "";
|
|
131
134
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
132
135
|
const controller = new AbortController();
|
|
@@ -24,9 +24,12 @@ import {
|
|
|
24
24
|
usageRoot,
|
|
25
25
|
hasV1UsageFields,
|
|
26
26
|
formatQuota,
|
|
27
|
+
newApiTokenQuota,
|
|
27
28
|
formatNewApiTokenLine,
|
|
28
29
|
formatOneApiBillingLine,
|
|
30
|
+
hasSpendableOneApiLimit,
|
|
29
31
|
formatOpenRouterLine,
|
|
32
|
+
formatClaudeCodeHubLine,
|
|
30
33
|
} from "./format.mjs";
|
|
31
34
|
import { readRouteCache } from "./cache.mjs";
|
|
32
35
|
|
|
@@ -66,13 +69,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
66
69
|
key: context.key,
|
|
67
70
|
name: "New API token usage",
|
|
68
71
|
});
|
|
69
|
-
const
|
|
70
|
-
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
71
|
-
const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
|
|
72
|
-
let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
|
|
73
|
-
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
74
|
-
remaining = Math.max(0, quota - used);
|
|
75
|
-
}
|
|
72
|
+
const { quota, used, remaining } = newApiTokenQuota(json);
|
|
76
73
|
const scale = await newApiQuotaScale();
|
|
77
74
|
const quotaForWarning = scale ? quota / scale : quota;
|
|
78
75
|
const usedForWarning = scale ? used / scale : used;
|
|
@@ -109,13 +106,16 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
109
106
|
throw new Error("One API billing payload has no quota fields");
|
|
110
107
|
}
|
|
111
108
|
const used = usageCents / 100;
|
|
109
|
+
const spendableLimit = hasSpendableOneApiLimit(limit);
|
|
112
110
|
const normalized = {
|
|
113
|
-
mode: "quota_limited",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
mode: spendableLimit ? "quota_limited" : "unrestricted",
|
|
112
|
+
...(spendableLimit ? {
|
|
113
|
+
quota: {
|
|
114
|
+
limit,
|
|
115
|
+
used,
|
|
116
|
+
remaining: Math.max(0, limit - used),
|
|
117
|
+
},
|
|
118
|
+
} : { used }),
|
|
119
119
|
unit: "USD",
|
|
120
120
|
source: "oneapi-billing",
|
|
121
121
|
raw: { subscription, usage },
|
|
@@ -128,6 +128,24 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Claude Code Hub exposes self-scoped quota windows to the same API key used
|
|
132
|
+
// for model requests. The endpoint accepts the existing Bearer authentication.
|
|
133
|
+
async function fetchClaudeCodeHubUsage(context) {
|
|
134
|
+
const base = cleanBaseUrl(context.baseUrl)
|
|
135
|
+
.replace(/\/api\/v1$/i, "")
|
|
136
|
+
.replace(/\/v1$/i, "");
|
|
137
|
+
const json = await requestJson(joinUrl(base, "/api/v1/me/quota"), {
|
|
138
|
+
key: context.key,
|
|
139
|
+
name: "Claude Code Hub quota",
|
|
140
|
+
});
|
|
141
|
+
return usageResult(
|
|
142
|
+
context,
|
|
143
|
+
"claude-code-hub",
|
|
144
|
+
formatClaudeCodeHubLine(json),
|
|
145
|
+
json
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
// OpenRouter exposes normal API-key usage at /api/v1/key. Some accounts also
|
|
132
150
|
// expose credits at /api/v1/credits; keep this route isolated because
|
|
133
151
|
// OpenRouter's base URL already includes /api/v1, unlike New API.
|
|
@@ -173,6 +191,11 @@ const USAGE_ROUTES = {
|
|
|
173
191
|
path: "/api/v1/key",
|
|
174
192
|
run: fetchOpenRouterUsage,
|
|
175
193
|
},
|
|
194
|
+
"claude-code-hub": {
|
|
195
|
+
id: "claude-code-hub",
|
|
196
|
+
path: "/api/v1/me/quota",
|
|
197
|
+
run: fetchClaudeCodeHubUsage,
|
|
198
|
+
},
|
|
176
199
|
};
|
|
177
200
|
|
|
178
201
|
// User-authored gateway routes, declared in config.jsonc:
|
|
@@ -256,7 +279,8 @@ async function routeRegistry() {
|
|
|
256
279
|
return registry;
|
|
257
280
|
}
|
|
258
281
|
|
|
259
|
-
// Presets select API-key usage protocols
|
|
282
|
+
// Presets primarily select API-key usage protocols. Brand aliases map to a
|
|
283
|
+
// shared protocol when the upstream gateway exposes the same endpoints.
|
|
260
284
|
async function usageRouteIds(context) {
|
|
261
285
|
const preset = await usagePreset();
|
|
262
286
|
const routes = {
|
|
@@ -264,7 +288,10 @@ async function usageRouteIds(context) {
|
|
|
264
288
|
"openai-compatible": ["v1-usage"],
|
|
265
289
|
"new-api": ["newapi-token"],
|
|
266
290
|
"one-api": ["oneapi-billing"],
|
|
291
|
+
"one-hub": ["oneapi-billing"],
|
|
292
|
+
"done-hub": ["oneapi-billing"],
|
|
267
293
|
"openrouter": ["openrouter"],
|
|
294
|
+
"claude-code-hub": ["claude-code-hub"],
|
|
268
295
|
};
|
|
269
296
|
if (routes[preset]) return routes[preset];
|
|
270
297
|
|
|
@@ -275,7 +302,7 @@ async function usageRouteIds(context) {
|
|
|
275
302
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
276
303
|
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai")
|
|
277
304
|
? ["openrouter"]
|
|
278
|
-
: ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
305
|
+
: ["v1-usage", "newapi-token", "oneapi-billing", "claude-code-hub"];
|
|
279
306
|
return [...new Set([...customIds, ...builtinIds])];
|
|
280
307
|
}
|
|
281
308
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kairyou/agent-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"config.default.jsonc",
|
|
18
|
+
"docs/",
|
|
18
19
|
"dist/",
|
|
19
20
|
"integrations/",
|
|
20
21
|
"scripts/",
|
package/scripts/install.mjs
CHANGED
|
@@ -57,6 +57,15 @@ const INSTALL_ROOT =
|
|
|
57
57
|
process.env.AGENT_TOOLS_HOME || path.join(os.homedir(), ".agent-tools");
|
|
58
58
|
const META_KEY = "_agentTools";
|
|
59
59
|
const META_VERSION = 1;
|
|
60
|
+
// Stamped into install-state.json so a later install can tell which release
|
|
61
|
+
// wrote the current layout. Purely informational today.
|
|
62
|
+
const PACKAGE_VERSION = (() => {
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, "package.json"), "utf8")).version || "";
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
})();
|
|
60
69
|
// Everything copied into ~/.agent-tools is built output from dist/ (see
|
|
61
70
|
// scripts/build.mjs); integrations/ holds the sources.
|
|
62
71
|
const SOURCE = {
|
|
@@ -395,14 +404,13 @@ function removeFile(file, dryRun) {
|
|
|
395
404
|
console.log(` removed ${file}`);
|
|
396
405
|
}
|
|
397
406
|
|
|
398
|
-
function usageEntry() {
|
|
407
|
+
function usageEntry({ silent = false } = {}) {
|
|
399
408
|
return {
|
|
400
409
|
hooks: [
|
|
401
410
|
{
|
|
402
411
|
type: "command",
|
|
403
|
-
command: nodeCmd(RUNTIME.codexUsageHook)
|
|
412
|
+
command: `${nodeCmd(RUNTIME.codexUsageHook)}${silent ? " --silent" : ""}`,
|
|
404
413
|
timeout: 5,
|
|
405
|
-
statusMessage: "Refreshing API usage",
|
|
406
414
|
},
|
|
407
415
|
],
|
|
408
416
|
};
|
|
@@ -433,7 +441,7 @@ function applyProviderUsage(cfg, { remove }) {
|
|
|
433
441
|
}
|
|
434
442
|
cfg.hooks[event] = cfg.hooks[event] || [];
|
|
435
443
|
cfg.hooks[event] = cfg.hooks[event].filter((entry) => !isOurProviderUsageEntry(entry));
|
|
436
|
-
cfg.hooks[event].push(usageEntry());
|
|
444
|
+
cfg.hooks[event].push(usageEntry({ silent: event === "UserPromptSubmit" }));
|
|
437
445
|
}
|
|
438
446
|
if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks;
|
|
439
447
|
}
|
|
@@ -631,6 +639,7 @@ function managedSkillStatus(dest, identity) {
|
|
|
631
639
|
|
|
632
640
|
function recordManagedSkill(dest, identity) {
|
|
633
641
|
const state = readInstallState();
|
|
642
|
+
state.packageVersion = PACKAGE_VERSION;
|
|
634
643
|
state.artifacts[skillManifestKey(dest)] = {
|
|
635
644
|
path: fwd(path.resolve(dest)),
|
|
636
645
|
...identity,
|
package/scripts/publish.mjs
CHANGED
|
@@ -41,8 +41,12 @@ if (typeof PACKAGE_NAME !== "string" || PACKAGE_NAME.trim() === "") {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function runNpm(args, { capture = false, allowFailure = false } = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// npm run sets npm_execpath. When invoked directly (`node scripts/...`), find
|
|
45
|
+
// the bundled npm-cli.js next to node — spawning npm.cmd fails on Node 22+.
|
|
46
|
+
// Version-manager layouts may not have it there, so fall back to PATH.
|
|
47
|
+
const bundledNpm = path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
48
|
+
const npmExecPath = process.env.npm_execpath || (fs.existsSync(bundledNpm) ? bundledNpm : undefined);
|
|
49
|
+
const command = npmExecPath ? process.execPath : "npm";
|
|
46
50
|
const commandArgs = npmExecPath ? [npmExecPath, ...args] : args;
|
|
47
51
|
const result = spawnSync(command, commandArgs, {
|
|
48
52
|
cwd: ROOT,
|
package/scripts/release.mjs
CHANGED
|
@@ -78,12 +78,14 @@ function run(command, args, { label, onFailure } = {}) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function runNpm(args) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
// npm run sets npm_execpath. When invoked directly (`node scripts/...`), find
|
|
82
|
+
// the bundled npm-cli.js next to node — spawning npm.cmd fails on Node 22+.
|
|
83
|
+
// Version-manager layouts may not have it there, so fall back to PATH.
|
|
84
|
+
const bundledNpm = path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
85
|
+
const npmExecPath = process.env.npm_execpath || (fs.existsSync(bundledNpm) ? bundledNpm : undefined);
|
|
86
|
+
const command = npmExecPath ? process.execPath : "npm";
|
|
87
|
+
const commandArgs = npmExecPath ? [npmExecPath, ...args] : args;
|
|
88
|
+
run(command, commandArgs, { label: `npm ${args.join(" ")}` });
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
function printRecovery(commands) {
|