@kairyou/agent-tools 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  import { pathToFileURL as pathToFileURL2 } from "node:url";
5
5
 
6
6
  // integrations/usage/lib/config.mjs
7
- import { readFile, writeFile, mkdir } from "node:fs/promises";
7
+ import { readFile, writeFile, mkdir, open, stat } from "node:fs/promises";
8
8
  import { existsSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { homedir } from "node:os";
@@ -905,10 +905,28 @@ async function agentConfig() {
905
905
  }
906
906
  return agentConfigCache;
907
907
  }
908
+ var DEBUG_LOG_MAX_BYTES = 256 * 1024;
909
+ var DEBUG_LOG_KEEP_BYTES = 128 * 1024;
910
+ async function rotateDebugLogIfNeeded() {
911
+ try {
912
+ const { size } = await stat(DEBUG_PATH);
913
+ if (size <= DEBUG_LOG_MAX_BYTES) return;
914
+ const handle = await open(DEBUG_PATH, "r");
915
+ try {
916
+ const buffer = Buffer.alloc(DEBUG_LOG_KEEP_BYTES);
917
+ await handle.read(buffer, 0, DEBUG_LOG_KEEP_BYTES, size - DEBUG_LOG_KEEP_BYTES);
918
+ await writeFile(DEBUG_PATH, buffer.toString("utf8").replace(/^[^\n]*\n?/, ""));
919
+ } finally {
920
+ await handle.close();
921
+ }
922
+ } catch {
923
+ }
924
+ }
908
925
  async function debugLog(event) {
909
926
  const config = await agentConfig();
910
927
  if (process.env.PROVIDER_USAGE_DEBUG !== "1" && config.debug !== true) return;
911
928
  await mkdir(dirname(DEBUG_PATH), { recursive: true });
929
+ await rotateDebugLogIfNeeded();
912
930
  const line = JSON.stringify({
913
931
  at: (/* @__PURE__ */ new Date()).toISOString(),
914
932
  ...event
@@ -919,33 +937,13 @@ async function debugLog(event) {
919
937
  async function providerUsageDays() {
920
938
  const config = await agentConfig();
921
939
  const value = Number(process.env.PROVIDER_USAGE_DAYS || config.days || DEFAULT_USAGE_DAYS);
922
- if (!Number.isInteger(value) || value <= 0 || value > MAX_USAGE_DAYS) return DEFAULT_USAGE_DAYS;
923
- return value;
940
+ if (!Number.isInteger(value) || value <= 0) return DEFAULT_USAGE_DAYS;
941
+ return Math.min(value, MAX_USAGE_DAYS);
924
942
  }
925
943
  async function usagePreset() {
926
944
  const config = await agentConfig();
927
945
  return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
928
946
  }
929
- async function panelUserId() {
930
- const config = await agentConfig();
931
- const raw = process.env.PROVIDER_USAGE_USER_ID || config.userId || "";
932
- const parsed = Number.parseInt(String(raw), 10);
933
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
934
- }
935
- async function panelUserHeaders() {
936
- const userId = await panelUserId();
937
- if (!userId) return {};
938
- const value = String(userId);
939
- return {
940
- "New-API-User": value,
941
- "Veloera-User": value,
942
- "voapi-user": value,
943
- "User-id": value,
944
- "X-User-Id": value,
945
- "Rix-Api-User": value,
946
- "neo-api-user": value
947
- };
948
- }
949
947
  function snapshotTtlMs() {
950
948
  const raw = Number(process.env.AGENT_TOOLS_USAGE_SNAPSHOT_TTL_MS);
951
949
  return Number.isFinite(raw) && raw >= 0 ? raw : 6e4;
@@ -1205,11 +1203,11 @@ function mergeSetCookiePairs(cookieHeader, setCookieHeaders) {
1205
1203
  return merged;
1206
1204
  }
1207
1205
  async function requestJson(url, options = {}) {
1208
- const { key = "", headers = {}, name = "usage" } = options;
1206
+ const { key = "", headers = {}, name = "usage", timeoutMs = REQUEST_TIMEOUT_MS } = options;
1209
1207
  let cookieHeader = "";
1210
1208
  for (let attempt = 0; attempt < 2; attempt += 1) {
1211
1209
  const controller = new AbortController();
1212
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1210
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
1213
1211
  const response = await fetch(url, {
1214
1212
  headers: {
1215
1213
  accept: "application/json",
@@ -1388,36 +1386,8 @@ function formatOpenRouterLine(label, data) {
1388
1386
  if (parts.length === 1) throw new Error("OpenRouter payload has no usage fields");
1389
1387
  return parts.join(" | ");
1390
1388
  }
1391
- function panelQuotaScale(kind) {
1392
- return kind === "veloera" ? 1e6 : DEFAULT_NEW_API_QUOTA_SCALE;
1393
- }
1394
- function panelQuotaLooksRemaining(kind) {
1395
- return ["new-api", "anyrouter", "agentrouter", "done-hub", "donehub"].includes(kind);
1396
- }
1397
- async function formatPanelUserSelfLine(label, data, kind) {
1398
- const root = usageRoot(data);
1399
- const scale = panelQuotaScale(kind);
1400
- const quota = pickNumber(root, ["quota"]);
1401
- const used = pickNumber(root, ["used_quota", "usedQuota"]);
1402
- const todayIncome = pickNumber(root, ["today_income", "todayIncome"]);
1403
- const todayUsed = pickNumber(root, ["today_quota_consumption", "todayQuotaConsumption"]);
1404
- if (quota === void 0 && used === void 0) {
1405
- throw new Error("panel /api/user/self payload has no quota fields");
1406
- }
1407
- const quotaUsd = quota === void 0 ? void 0 : quota / scale;
1408
- const usedUsd = used === void 0 ? void 0 : used / scale;
1409
- const remainingUsd = panelQuotaLooksRemaining(kind) ? quotaUsd : quotaUsd === void 0 || usedUsd === void 0 ? quotaUsd : Math.max(0, quotaUsd - usedUsd);
1410
- const totalUsd = panelQuotaLooksRemaining(kind) ? quotaUsd === void 0 || usedUsd === void 0 ? quotaUsd : quotaUsd + usedUsd : quotaUsd;
1411
- const parts = usageParts();
1412
- if (remainingUsd !== void 0) parts.push(`balance ${formatMoney(remainingUsd)}`);
1413
- if (usedUsd !== void 0 && totalUsd !== void 0) {
1414
- parts.push(`used ${formatMoney(usedUsd)}/${formatMoney(totalUsd)}`);
1415
- } else if (usedUsd !== void 0) {
1416
- parts.push(`used ${formatMoney(usedUsd)}`);
1417
- }
1418
- if (todayUsed !== void 0) parts.push(`today ${formatMoney(todayUsed / scale)}`);
1419
- if (todayIncome !== void 0) parts.push(`income ${formatMoney(todayIncome / scale)}`);
1420
- return parts.join(" | ");
1389
+ function formatOneApiBillingLine(limit, used) {
1390
+ return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
1421
1391
  }
1422
1392
  function formatQuotaLimitedLine(label, root) {
1423
1393
  const quota = root?.quota || {};
@@ -1497,7 +1467,7 @@ async function fetchV1Usage(context) {
1497
1467
  async function fetchNewApiTokenUsage(context) {
1498
1468
  const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/usage/token/"), {
1499
1469
  key: context.key,
1500
- name: "NewAPI token usage"
1470
+ name: "New API token usage"
1501
1471
  });
1502
1472
  const root = usageRoot(json);
1503
1473
  const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
@@ -1523,66 +1493,37 @@ async function fetchNewApiTokenUsage(context) {
1523
1493
  };
1524
1494
  return usageResult(context, "newapi-token", await formatNewApiTokenLine(context.label, json), normalized);
1525
1495
  }
1526
- async function fetchPanelUserSelfUsage(context) {
1527
- const preset = await usagePreset();
1528
- const kind = preset === "auto" ? "new-api" : preset;
1529
- const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/user/self"), {
1496
+ async function fetchOneApiBillingUsage(context) {
1497
+ const base = serviceRoot(context.baseUrl);
1498
+ const subscription = await requestJson(joinUrl(base, "/v1/dashboard/billing/subscription"), {
1530
1499
  key: context.key,
1531
- name: "panel /api/user/self",
1532
- headers: await panelUserHeaders()
1500
+ name: "One API billing subscription"
1533
1501
  });
1534
- const root = usageRoot(json);
1535
- if (pickNumber(root, ["quota"]) === void 0 && pickNumber(root, ["used_quota", "usedQuota"]) === void 0) {
1536
- await debugLog({
1537
- source: "panel /api/user/self",
1538
- payloadKeys: Object.keys(root || {}).slice(0, 20),
1539
- success: root?.success,
1540
- message: root?.message || root?.error?.message || ""
1541
- });
1502
+ const usage = await requestJson(joinUrl(base, "/v1/dashboard/billing/usage"), {
1503
+ key: context.key,
1504
+ name: "One API billing usage"
1505
+ });
1506
+ const limit = pickNumber(subscription, ["hard_limit_usd", "hardLimitUsd"]);
1507
+ const usageCents = pickNumber(usage, ["total_usage", "totalUsage"]);
1508
+ if (limit === void 0 || usageCents === void 0) {
1509
+ throw new Error("One API billing payload has no quota fields");
1542
1510
  }
1543
- const scale = panelQuotaScale(kind);
1544
- const quota = pickNumber(root, ["quota"]);
1545
- const used = pickNumber(root, ["used_quota", "usedQuota"]);
1546
- const remaining = panelQuotaLooksRemaining(kind) ? quota : quota === void 0 || used === void 0 ? quota : Math.max(0, quota - used);
1547
- const total = panelQuotaLooksRemaining(kind) ? quota === void 0 || used === void 0 ? quota : quota + used : quota;
1511
+ const used = usageCents / 100;
1548
1512
  const normalized = {
1549
1513
  mode: "quota_limited",
1550
1514
  quota: {
1551
- limit: total === void 0 ? void 0 : total / scale,
1552
- used: used === void 0 ? void 0 : used / scale,
1553
- remaining: remaining === void 0 ? void 0 : remaining / scale
1515
+ limit,
1516
+ used,
1517
+ remaining: Math.max(0, limit - used)
1554
1518
  },
1555
1519
  unit: "USD",
1556
- source: "panel-user-self",
1557
- raw: json
1558
- };
1559
- return usageResult(
1560
- context,
1561
- "panel-user-self",
1562
- await formatPanelUserSelfLine(context.label, json, kind),
1563
- normalized
1564
- );
1565
- }
1566
- async function fetchSub2ApiAuthMeUsage(context) {
1567
- const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/v1/auth/me"), {
1568
- key: context.key,
1569
- name: "Sub2API auth/me"
1570
- });
1571
- const root = usageRoot(json);
1572
- const balance = pickNumber(root, ["balance"]);
1573
- if (balance === void 0) throw new Error("Sub2API auth/me payload has no balance field");
1574
- const normalized = {
1575
- mode: "unrestricted",
1576
- planName: root?.username || root?.email || context.label || "Sub2API",
1577
- balance,
1578
- unit: "USD",
1579
- source: "sub2api-auth-me",
1580
- raw: json
1520
+ source: "oneapi-billing",
1521
+ raw: { subscription, usage }
1581
1522
  };
1582
1523
  return usageResult(
1583
1524
  context,
1584
- "sub2api-auth-me",
1585
- `API | balance ${formatMoney(balance)}`,
1525
+ "oneapi-billing",
1526
+ formatOneApiBillingLine(limit, used),
1586
1527
  normalized
1587
1528
  );
1588
1529
  }
@@ -1610,20 +1551,15 @@ var USAGE_ROUTES = {
1610
1551
  path: "/v1/usage",
1611
1552
  run: fetchV1Usage
1612
1553
  },
1613
- "sub2api-auth-me": {
1614
- id: "sub2api-auth-me",
1615
- path: "/api/v1/auth/me",
1616
- run: fetchSub2ApiAuthMeUsage
1617
- },
1618
1554
  "newapi-token": {
1619
1555
  id: "newapi-token",
1620
1556
  path: "/api/usage/token/",
1621
1557
  run: fetchNewApiTokenUsage
1622
1558
  },
1623
- "panel-user-self": {
1624
- id: "panel-user-self",
1625
- path: "/api/user/self",
1626
- run: fetchPanelUserSelfUsage
1559
+ "oneapi-billing": {
1560
+ id: "oneapi-billing",
1561
+ path: "/v1/dashboard/billing/subscription",
1562
+ run: fetchOneApiBillingUsage
1627
1563
  },
1628
1564
  "openrouter": {
1629
1565
  id: "openrouter",
@@ -1697,23 +1633,16 @@ async function routeRegistry() {
1697
1633
  async function usageRouteIds(context) {
1698
1634
  const preset = await usagePreset();
1699
1635
  const routes = {
1700
- "sub2api": ["v1-usage", "sub2api-auth-me"],
1636
+ "sub2api": ["v1-usage"],
1701
1637
  "openai-compatible": ["v1-usage"],
1702
- "new-api": ["newapi-token", "panel-user-self"],
1703
- "one-api": ["newapi-token", "panel-user-self"],
1704
- "onehub": ["newapi-token", "panel-user-self"],
1705
- "one-hub": ["newapi-token", "panel-user-self"],
1706
- "donehub": ["newapi-token", "panel-user-self"],
1707
- "done-hub": ["newapi-token", "panel-user-self"],
1708
- "veloera": ["panel-user-self", "newapi-token"],
1709
- "anyrouter": ["newapi-token", "panel-user-self", "v1-usage"],
1710
- "agentrouter": ["newapi-token", "panel-user-self", "v1-usage"],
1638
+ "new-api": ["newapi-token"],
1639
+ "one-api": ["oneapi-billing"],
1711
1640
  "openrouter": ["openrouter"]
1712
1641
  };
1713
1642
  if (routes[preset]) return routes[preset];
1714
1643
  if (preset !== "auto") return (await routeRegistry())[preset] ? [preset] : [];
1715
1644
  const customIds = (await customRoutes()).map((route) => route.id);
1716
- const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "sub2api-auth-me", "newapi-token", "panel-user-self"];
1645
+ const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing"];
1717
1646
  return [.../* @__PURE__ */ new Set([...customIds, ...builtinIds])];
1718
1647
  }
1719
1648
  async function cachedUsageRoute(context, registry) {
@@ -2,6 +2,7 @@
2
2
  // Local CLI used by the managed at-usage skills.
3
3
 
4
4
  import { queryAgentProviderUsage } from "./core.mjs";
5
+ import { debugLog } from "./lib/config.mjs";
5
6
 
6
7
  function parseAgent(argv) {
7
8
  for (let index = 0; index < argv.length; index += 1) {
@@ -20,8 +21,10 @@ if (agent !== "claude" && agent !== "codex") {
20
21
  try {
21
22
  const result = await queryAgentProviderUsage(agent);
22
23
  if (result?.text) process.stdout.write(`${result.text}\n`);
23
- } catch {
24
+ } catch (error) {
24
25
  // Usage is informational. Leave stdout empty so the skill can report the
25
- // provider as unavailable without exposing endpoint or credential details.
26
+ // provider as unavailable without exposing endpoint or credential details;
27
+ // with providerUsage.debug the error lands in logs/usage-debug.log.
28
+ await debugLog({ source: "cli", agent, error: error?.message || String(error) }).catch(() => {});
26
29
  }
27
30
  }
@@ -1,7 +1,7 @@
1
1
  // Paths, constants, config.jsonc access, and debug logging shared by the
2
2
  // usage runtime modules.
3
3
 
4
- import { readFile, writeFile, mkdir } from "node:fs/promises";
4
+ import { readFile, writeFile, mkdir, open, stat } from "node:fs/promises";
5
5
  import { existsSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { homedir } from "node:os";
@@ -47,10 +47,31 @@ export async function agentConfig() {
47
47
  return agentConfigCache;
48
48
  }
49
49
 
50
+ const DEBUG_LOG_MAX_BYTES = 256 * 1024;
51
+ const DEBUG_LOG_KEEP_BYTES = 128 * 1024;
52
+
53
+ async function rotateDebugLogIfNeeded() {
54
+ try {
55
+ const { size } = await stat(DEBUG_PATH);
56
+ if (size <= DEBUG_LOG_MAX_BYTES) return;
57
+ const handle = await open(DEBUG_PATH, "r");
58
+ try {
59
+ const buffer = Buffer.alloc(DEBUG_LOG_KEEP_BYTES);
60
+ await handle.read(buffer, 0, DEBUG_LOG_KEEP_BYTES, size - DEBUG_LOG_KEEP_BYTES);
61
+ await writeFile(DEBUG_PATH, buffer.toString("utf8").replace(/^[^\n]*\n?/, ""));
62
+ } finally {
63
+ await handle.close();
64
+ }
65
+ } catch {
66
+ // Rotation is best-effort; appending must not fail because of it.
67
+ }
68
+ }
69
+
50
70
  export async function debugLog(event) {
51
71
  const config = await agentConfig();
52
72
  if (process.env.PROVIDER_USAGE_DEBUG !== "1" && config.debug !== true) return;
53
73
  await mkdir(dirname(DEBUG_PATH), { recursive: true });
74
+ await rotateDebugLogIfNeeded();
54
75
  const line = JSON.stringify({
55
76
  at: new Date().toISOString(),
56
77
  ...event,
@@ -61,8 +82,8 @@ export async function debugLog(event) {
61
82
  export async function providerUsageDays() {
62
83
  const config = await agentConfig();
63
84
  const value = Number(process.env.PROVIDER_USAGE_DAYS || config.days || DEFAULT_USAGE_DAYS);
64
- if (!Number.isInteger(value) || value <= 0 || value > MAX_USAGE_DAYS) return DEFAULT_USAGE_DAYS;
65
- return value;
85
+ if (!Number.isInteger(value) || value <= 0) return DEFAULT_USAGE_DAYS;
86
+ return Math.min(value, MAX_USAGE_DAYS);
66
87
  }
67
88
 
68
89
  export async function usagePreset() {
@@ -70,28 +91,6 @@ export async function usagePreset() {
70
91
  return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
71
92
  }
72
93
 
73
- export async function panelUserId() {
74
- const config = await agentConfig();
75
- const raw = process.env.PROVIDER_USAGE_USER_ID || config.userId || "";
76
- const parsed = Number.parseInt(String(raw), 10);
77
- return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
78
- }
79
-
80
- export async function panelUserHeaders() {
81
- const userId = await panelUserId();
82
- if (!userId) return {};
83
- const value = String(userId);
84
- return {
85
- "New-API-User": value,
86
- "Veloera-User": value,
87
- "voapi-user": value,
88
- "User-id": value,
89
- "X-User-Id": value,
90
- "Rix-Api-User": value,
91
- "neo-api-user": value,
92
- };
93
- }
94
-
95
94
  // Passive callers (the codex hook fires per prompt; several sessions may run
96
95
  // at once) reuse a fresh snapshot instead of hitting the gateway every time.
97
96
  // Same knob the statusline uses; 0 disables.
@@ -1,6 +1,6 @@
1
1
  // Turns gateway payloads into the compact one-line usage message.
2
2
 
3
- import { newApiQuotaScale, providerUsageDays, DEFAULT_NEW_API_QUOTA_SCALE } from "./config.mjs";
3
+ import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
4
4
 
5
5
  export function pickNumber(obj, keys) {
6
6
  for (const key of keys) {
@@ -159,45 +159,8 @@ export function formatOpenRouterLine(label, data) {
159
159
  return parts.join(" | ");
160
160
  }
161
161
 
162
- export function panelQuotaScale(kind) {
163
- return kind === "veloera" ? 1000000 : DEFAULT_NEW_API_QUOTA_SCALE;
164
- }
165
-
166
- export function panelQuotaLooksRemaining(kind) {
167
- return ["new-api", "anyrouter", "agentrouter", "done-hub", "donehub"].includes(kind);
168
- }
169
-
170
- export async function formatPanelUserSelfLine(label, data, kind) {
171
- const root = usageRoot(data);
172
- const scale = panelQuotaScale(kind);
173
- const quota = pickNumber(root, ["quota"]);
174
- const used = pickNumber(root, ["used_quota", "usedQuota"]);
175
- const todayIncome = pickNumber(root, ["today_income", "todayIncome"]);
176
- const todayUsed = pickNumber(root, ["today_quota_consumption", "todayQuotaConsumption"]);
177
-
178
- if (quota === undefined && used === undefined) {
179
- throw new Error("panel /api/user/self payload has no quota fields");
180
- }
181
-
182
- const quotaUsd = quota === undefined ? undefined : quota / scale;
183
- const usedUsd = used === undefined ? undefined : used / scale;
184
- const remainingUsd = panelQuotaLooksRemaining(kind)
185
- ? quotaUsd
186
- : (quotaUsd === undefined || usedUsd === undefined ? quotaUsd : Math.max(0, quotaUsd - usedUsd));
187
- const totalUsd = panelQuotaLooksRemaining(kind)
188
- ? (quotaUsd === undefined || usedUsd === undefined ? quotaUsd : quotaUsd + usedUsd)
189
- : quotaUsd;
190
-
191
- const parts = usageParts();
192
- if (remainingUsd !== undefined) parts.push(`balance ${formatMoney(remainingUsd)}`);
193
- if (usedUsd !== undefined && totalUsd !== undefined) {
194
- parts.push(`used ${formatMoney(usedUsd)}/${formatMoney(totalUsd)}`);
195
- } else if (usedUsd !== undefined) {
196
- parts.push(`used ${formatMoney(usedUsd)}`);
197
- }
198
- if (todayUsed !== undefined) parts.push(`today ${formatMoney(todayUsed / scale)}`);
199
- if (todayIncome !== undefined) parts.push(`income ${formatMoney(todayIncome / scale)}`);
200
- return parts.join(" | ");
162
+ export function formatOneApiBillingLine(limit, used) {
163
+ return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
201
164
  }
202
165
 
203
166
  function formatQuotaLimitedLine(label, root) {
@@ -126,11 +126,11 @@ function mergeSetCookiePairs(cookieHeader, setCookieHeaders) {
126
126
  }
127
127
 
128
128
  export async function requestJson(url, options = {}) {
129
- const { key = "", headers = {}, name = "usage" } = options;
129
+ const { key = "", headers = {}, name = "usage", timeoutMs = REQUEST_TIMEOUT_MS } = options;
130
130
  let cookieHeader = "";
131
131
  for (let attempt = 0; attempt < 2; attempt += 1) {
132
132
  const controller = new AbortController();
133
- const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
133
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
134
134
  const response = await fetch(url, {
135
135
  headers: {
136
136
  accept: "application/json",