@kairyou/agent-tools 0.9.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.
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // integrations/usage/core.mjs
4
- import { pathToFileURL as pathToFileURL2 } from "node:url";
4
+ import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
5
+ import { spawn } from "node:child_process";
5
6
 
6
7
  // integrations/usage/lib/config.mjs
7
8
  import { readFile, writeFile, mkdir, open, stat } from "node:fs/promises";
@@ -875,9 +876,10 @@ var AUTH_PATH = join(CODEX_HOME, "auth.json");
875
876
  var CODEX_CONFIG_PATH = join(CODEX_HOME, "config.toml");
876
877
  var AGENT_CONFIG_PATH = join(AGENT_TOOLS_HOME, "config.jsonc");
877
878
  var DEBUG_PATH = join(AGENT_TOOLS_HOME, "logs", "usage-debug.log");
878
- var ROUTE_CACHE_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-routes.json");
879
- var SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
880
- var REFRESH_STATE_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-refresh-state.json");
879
+ var CACHE_DIR = join(AGENT_TOOLS_HOME, "cache");
880
+ var ROUTE_CACHE_PATH = join(CACHE_DIR, "usage-routes.json");
881
+ var SNAPSHOT_PATH = join(CACHE_DIR, "usage-snapshot.json");
882
+ var REFRESH_STATE_PATH = join(CACHE_DIR, "usage-refresh-state.json");
881
883
  var DEFAULT_USAGE_DAYS = 30;
882
884
  var MAX_USAGE_DAYS = 90;
883
885
  var DEFAULT_NEW_API_QUOTA_SCALE = 5e5;
@@ -944,10 +946,8 @@ async function usagePreset() {
944
946
  const config = await agentConfig();
945
947
  return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
946
948
  }
947
- function snapshotTtlMs() {
948
- const raw = Number(process.env.AGENT_TOOLS_USAGE_SNAPSHOT_TTL_MS);
949
- return Number.isFinite(raw) && raw >= 0 ? raw : 6e4;
950
- }
949
+ var HOOK_SNAPSHOT_MAX_AGE_MS = 10 * 6e4;
950
+ var REFRESH_INTERVAL_MS = 6e4;
951
951
  async function newApiQuotaScale() {
952
952
  const config = await agentConfig();
953
953
  const scale = Number(config.newApiQuotaScale || DEFAULT_NEW_API_QUOTA_SCALE);
@@ -995,114 +995,161 @@ function hostIncludes(baseUrl, value) {
995
995
  }
996
996
 
997
997
  // integrations/usage/lib/cache.mjs
998
- import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
999
- import { dirname as dirname2 } from "node:path";
1000
- var ROUTE_CACHE_VERSION = 1;
1001
- var SNAPSHOT_VERSION = 1;
1002
- var REFRESH_STATE_VERSION = 1;
1003
- async function readRouteCache() {
998
+ import { writeFile as writeFile2, mkdir as mkdir2, open as open2, unlink, rename, stat as stat2, utimes } from "node:fs/promises";
999
+ import { createHash, randomUUID } from "node:crypto";
1000
+ import { dirname as dirname2, join as join2 } from "node:path";
1001
+ var CACHE_VERSION = 1;
1002
+ var WRITE_LOCK_PATH = join2(CACHE_DIR, "usage-cache-write.lock");
1003
+ async function readJsonCache(path, field) {
1004
1004
  try {
1005
- const raw = await readTextIfExists(ROUTE_CACHE_PATH);
1006
- if (!raw.trim()) return { version: ROUTE_CACHE_VERSION, routes: {} };
1007
- const parsed = JSON.parse(raw);
1005
+ const raw = await readTextIfExists(path);
1006
+ const entries = raw.trim() ? JSON.parse(raw)?.[field] : null;
1008
1007
  return {
1009
- version: ROUTE_CACHE_VERSION,
1010
- routes: parsed?.routes && typeof parsed.routes === "object" ? parsed.routes : {}
1008
+ version: CACHE_VERSION,
1009
+ [field]: entries && typeof entries === "object" ? entries : {}
1011
1010
  };
1012
1011
  } catch {
1013
- return { version: ROUTE_CACHE_VERSION, routes: {} };
1012
+ return { version: CACHE_VERSION, [field]: {} };
1014
1013
  }
1015
1014
  }
1016
- async function rememberUsageRoute(context, route, result) {
1015
+ async function updateJsonCache(path, field, source, mutate) {
1016
+ const release = await acquireWriteLock();
1017
+ if (!release) {
1018
+ await debugLog({ source, skipped: "cache write lock unavailable" });
1019
+ return;
1020
+ }
1017
1021
  try {
1018
- const cache = await readRouteCache();
1019
- const key = usageRouteCacheKey(context.baseUrl);
1020
- cache.routes[key] = {
1021
- route: route.id,
1022
- path: route.path,
1023
- source: result.source,
1024
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1025
- };
1026
- await mkdir2(dirname2(ROUTE_CACHE_PATH), { recursive: true });
1027
- await writeFile2(ROUTE_CACHE_PATH, `${JSON.stringify(cache, null, 2)}
1022
+ const cache = await readJsonCache(path, field);
1023
+ mutate(cache[field]);
1024
+ await mkdir2(dirname2(path), { recursive: true });
1025
+ const temp = `${path}.${process.pid}.tmp`;
1026
+ await writeFile2(temp, `${JSON.stringify(cache, null, 2)}
1028
1027
  `);
1028
+ await rename(temp, path);
1029
1029
  } catch (error) {
1030
- await debugLog({ source: "route-cache", error: error.message });
1030
+ await debugLog({ source, error: error.message });
1031
+ } finally {
1032
+ await release();
1031
1033
  }
1032
1034
  }
1033
- async function readSnapshotCache() {
1034
- try {
1035
- const raw = await readTextIfExists(SNAPSHOT_PATH);
1036
- if (!raw.trim()) return { version: SNAPSHOT_VERSION, items: {} };
1037
- const parsed = JSON.parse(raw);
1038
- return {
1039
- version: SNAPSHOT_VERSION,
1040
- items: parsed?.items && typeof parsed.items === "object" ? parsed.items : {}
1035
+ async function readRouteCache() {
1036
+ return await readJsonCache(ROUTE_CACHE_PATH, "routes");
1037
+ }
1038
+ async function rememberUsageRoute(context, route, result) {
1039
+ await updateJsonCache(ROUTE_CACHE_PATH, "routes", "route-cache", (routes) => {
1040
+ routes[usageRouteCacheKey(context.baseUrl)] = {
1041
+ route: route.id,
1042
+ path: route.path,
1043
+ source: result.source,
1044
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1041
1045
  };
1042
- } catch {
1043
- return { version: SNAPSHOT_VERSION, items: {} };
1044
- }
1046
+ });
1045
1047
  }
1046
1048
  async function readUsageSnapshot(context) {
1047
- const cache = await readSnapshotCache();
1049
+ const cache = await readJsonCache(SNAPSHOT_PATH, "items");
1048
1050
  return cache.items[usageRouteCacheKey(context.baseUrl)] || null;
1049
1051
  }
1050
1052
  async function rememberUsageSnapshot(context, result) {
1051
1053
  if (!result?.text) return;
1052
- try {
1053
- const cache = await readSnapshotCache();
1054
- const key = usageRouteCacheKey(context.baseUrl);
1055
- cache.items[key] = {
1054
+ await updateJsonCache(SNAPSHOT_PATH, "items", "snapshot-cache", (items) => {
1055
+ items[usageRouteCacheKey(context.baseUrl)] = {
1056
1056
  text: result.text,
1057
1057
  source: result.source,
1058
1058
  baseUrl: context.baseUrl,
1059
1059
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1060
1060
  };
1061
- await mkdir2(dirname2(SNAPSHOT_PATH), { recursive: true });
1062
- await writeFile2(SNAPSHOT_PATH, `${JSON.stringify(cache, null, 2)}
1063
- `);
1064
- } catch (error) {
1065
- await debugLog({ source: "snapshot-cache", error: error.message });
1066
- }
1067
- }
1068
- async function readRefreshState() {
1069
- try {
1070
- const raw = await readTextIfExists(REFRESH_STATE_PATH);
1071
- if (!raw.trim()) return { version: REFRESH_STATE_VERSION, items: {} };
1072
- const parsed = JSON.parse(raw);
1073
- return {
1074
- version: REFRESH_STATE_VERSION,
1075
- items: parsed?.items && typeof parsed.items === "object" ? parsed.items : {}
1076
- };
1077
- } catch {
1078
- return { version: REFRESH_STATE_VERSION, items: {} };
1079
- }
1061
+ });
1080
1062
  }
1081
1063
  async function rememberRefreshState(context, patch) {
1082
- try {
1083
- const state = await readRefreshState();
1064
+ await updateJsonCache(REFRESH_STATE_PATH, "items", "refresh-state", (items) => {
1084
1065
  const key = usageRouteCacheKey(context.baseUrl);
1085
- state.items[key] = {
1086
- ...state.items[key] || {},
1087
- ...patch,
1088
- baseUrl: context.baseUrl
1089
- };
1090
- await mkdir2(dirname2(REFRESH_STATE_PATH), { recursive: true });
1091
- await writeFile2(REFRESH_STATE_PATH, `${JSON.stringify(state, null, 2)}
1092
- `);
1093
- } catch (error) {
1094
- await debugLog({ source: "refresh-state", error: error.message });
1066
+ items[key] = { ...items[key] || {}, ...patch, baseUrl: context.baseUrl };
1067
+ });
1068
+ }
1069
+ async function canRefreshUsage(context, minIntervalMs) {
1070
+ if (minIntervalMs <= 0) return true;
1071
+ const state = await readJsonCache(REFRESH_STATE_PATH, "items");
1072
+ const item = state.items[usageRouteCacheKey(context.baseUrl)] || {};
1073
+ const latest = Math.max(
1074
+ ...[item.lastStartedAt, item.lastSuccessAt, item.lastFailureAt].map((value) => Date.parse(value || "")).filter(Number.isFinite),
1075
+ 0
1076
+ );
1077
+ return latest === 0 || Date.now() - latest >= minIntervalMs;
1078
+ }
1079
+ async function tryLock(lockPath, staleMs, details = {}) {
1080
+ await mkdir2(dirname2(lockPath), { recursive: true });
1081
+ const token = randomUUID();
1082
+ for (let attempt = 0; attempt < 2; attempt += 1) {
1083
+ try {
1084
+ const handle = await open2(lockPath, "wx");
1085
+ try {
1086
+ await handle.writeFile(
1087
+ `${JSON.stringify({ token, pid: process.pid, ...details, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
1088
+ `
1089
+ );
1090
+ } finally {
1091
+ await handle.close();
1092
+ }
1093
+ const heartbeat = setInterval(() => {
1094
+ const now = /* @__PURE__ */ new Date();
1095
+ utimes(lockPath, now, now).catch(() => {
1096
+ });
1097
+ }, Math.max(50, Math.floor(staleMs / 3)));
1098
+ heartbeat.unref();
1099
+ return async () => {
1100
+ clearInterval(heartbeat);
1101
+ try {
1102
+ const held = JSON.parse(await readTextIfExists(lockPath));
1103
+ if (held?.token === token) await unlink(lockPath);
1104
+ } catch {
1105
+ }
1106
+ };
1107
+ } catch (error) {
1108
+ if (error?.code !== "EEXIST" || attempt > 0) return null;
1109
+ const age = await stat2(lockPath).then(
1110
+ ({ mtimeMs }) => Date.now() - mtimeMs,
1111
+ () => Infinity
1112
+ // vanished under us: retry immediately
1113
+ );
1114
+ if (age <= staleMs) return null;
1115
+ try {
1116
+ const claimed = `${lockPath}.${token}`;
1117
+ await rename(lockPath, claimed);
1118
+ await unlink(claimed).catch(() => {
1119
+ });
1120
+ } catch {
1121
+ return null;
1122
+ }
1123
+ }
1095
1124
  }
1125
+ return null;
1126
+ }
1127
+ function refreshLockPath(context) {
1128
+ const key = usageRouteCacheKey(context.baseUrl);
1129
+ const hash = createHash("sha256").update(key).digest("hex").slice(0, 16);
1130
+ return join2(CACHE_DIR, `usage-refresh-${hash}.lock`);
1131
+ }
1132
+ async function acquireUsageRefreshLease(context, leaseMs = 6e4) {
1133
+ return await tryLock(refreshLockPath(context), leaseMs, { baseUrl: context.baseUrl });
1134
+ }
1135
+ async function acquireWriteLock({ timeoutMs = 500, staleMs = 5e3 } = {}) {
1136
+ const deadline = Date.now() + timeoutMs;
1137
+ do {
1138
+ const release = await tryLock(WRITE_LOCK_PATH, staleMs);
1139
+ if (release) return release;
1140
+ await new Promise((resolve) => setTimeout(resolve, 10));
1141
+ } while (Date.now() < deadline);
1142
+ return null;
1096
1143
  }
1097
1144
 
1098
1145
  // integrations/usage/lib/routes.mjs
1099
1146
  import { readdir } from "node:fs/promises";
1100
- import { basename, extname, isAbsolute, join as join2 } from "node:path";
1147
+ import { basename, extname, isAbsolute, join as join3 } from "node:path";
1101
1148
  import { pathToFileURL } from "node:url";
1102
1149
 
1103
1150
  // integrations/usage/lib/http.mjs
1104
1151
  import { createContext, runInContext } from "node:vm";
1105
- var REQUEST_TIMEOUT_MS = 5e3;
1152
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
1106
1153
  var SHIELD_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
1107
1154
  function shortPreview(text) {
1108
1155
  return String(text || "").replace(/\s+/g, " ").trim().slice(0, 220);
@@ -1203,7 +1250,7 @@ function mergeSetCookiePairs(cookieHeader, setCookieHeaders) {
1203
1250
  return merged;
1204
1251
  }
1205
1252
  async function requestJson(url, options = {}) {
1206
- const { key = "", headers = {}, name = "usage", timeoutMs = REQUEST_TIMEOUT_MS } = options;
1253
+ const { key = "", headers = {}, name = "usage", timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = options;
1207
1254
  let cookieHeader = "";
1208
1255
  for (let attempt = 0; attempt < 2; attempt += 1) {
1209
1256
  const controller = new AbortController();
@@ -1682,11 +1729,11 @@ async function loadCustomRoutes() {
1682
1729
  const specs = Array.isArray(config.routes) ? config.routes : [];
1683
1730
  for (const spec of specs) {
1684
1731
  if (typeof spec !== "string" || !spec.trim()) continue;
1685
- const file = isAbsolute(spec) ? spec : join2(AGENT_TOOLS_HOME, spec);
1732
+ const file = isAbsolute(spec) ? spec : join3(AGENT_TOOLS_HOME, spec);
1686
1733
  const route = await loadRouteModule(file, spec);
1687
1734
  if (route) routes.push(route);
1688
1735
  }
1689
- const packagedDir = join2(AGENT_TOOLS_HOME, "dist", "usage", "routes");
1736
+ const packagedDir = join3(AGENT_TOOLS_HOME, "dist", "usage", "routes");
1690
1737
  let packaged = [];
1691
1738
  try {
1692
1739
  packaged = (await readdir(packagedDir)).filter((n) => n.endsWith(".mjs")).sort();
@@ -1694,7 +1741,7 @@ async function loadCustomRoutes() {
1694
1741
  packaged = [];
1695
1742
  }
1696
1743
  for (const name of packaged) {
1697
- const route = await loadRouteModule(join2(packagedDir, name), `dist/usage/routes/${name}`);
1744
+ const route = await loadRouteModule(join3(packagedDir, name), `dist/usage/routes/${name}`);
1698
1745
  if (route && !routes.some((existing) => existing.id === route.id)) routes.push(route);
1699
1746
  }
1700
1747
  return routes;
@@ -1848,7 +1895,7 @@ function normalizeUsageContext(input) {
1848
1895
 
1849
1896
  // integrations/usage/core.mjs
1850
1897
  function parseArgs(argv) {
1851
- const opts = { mode: "hook", agent: "codex" };
1898
+ const opts = { mode: "hook", agent: "codex", silent: false };
1852
1899
  let modeSet = false;
1853
1900
  for (let i = 0; i < argv.length; i += 1) {
1854
1901
  const arg = argv[i];
@@ -1856,6 +1903,8 @@ function parseArgs(argv) {
1856
1903
  opts.agent = argv[++i];
1857
1904
  } else if (arg.startsWith("--agent=")) {
1858
1905
  opts.agent = arg.slice("--agent=".length);
1906
+ } else if (arg === "--silent") {
1907
+ opts.silent = true;
1859
1908
  } else if (!arg.startsWith("-") && !modeSet) {
1860
1909
  opts.mode = arg;
1861
1910
  modeSet = true;
@@ -1931,27 +1980,59 @@ async function queryProviderUsage(input, options = {}) {
1931
1980
  return await queryUsageContext(normalizeUsageContext(input), options);
1932
1981
  }
1933
1982
  async function refresh(agent = "codex") {
1934
- return await queryAgentProviderUsage(agent);
1983
+ const context = await usageContext(agent);
1984
+ const release = await acquireUsageRefreshLease(context);
1985
+ if (!release) return { skipped: true, text: "" };
1986
+ try {
1987
+ if (!await canRefreshUsage(context, REFRESH_INTERVAL_MS)) return { skipped: true, text: "" };
1988
+ await rememberRefreshState(context, { lastStartedAt: (/* @__PURE__ */ new Date()).toISOString() });
1989
+ return await queryUsageContext(context, { agent, rememberSnapshot: true });
1990
+ } finally {
1991
+ await release();
1992
+ }
1993
+ }
1994
+ async function cachedUsage(context) {
1995
+ const cached = await readUsageSnapshot(context);
1996
+ const ageMs = cached?.updatedAt ? Date.now() - Date.parse(cached.updatedAt) : Infinity;
1997
+ return cached?.text && Number.isFinite(ageMs) ? { ...cached, ageMs } : null;
1935
1998
  }
1936
1999
  async function queryAgentProviderUsage(agent = "codex", { maxAgeMs = 0 } = {}) {
1937
2000
  const context = await usageContext(agent);
1938
2001
  if (maxAgeMs > 0) {
1939
- const cached = await readUsageSnapshot(context);
1940
- const age = cached?.updatedAt ? Date.now() - Date.parse(cached.updatedAt) : Infinity;
1941
- if (cached?.text && age < maxAgeMs) return { ...cached, cached: true };
2002
+ const cached = await cachedUsage(context);
2003
+ if (cached && cached.ageMs < maxAgeMs) return { ...cached, cached: true };
1942
2004
  }
1943
2005
  return await queryUsageContext(context, { agent, rememberSnapshot: true });
1944
2006
  }
2007
+ function scheduleRefresh(agent) {
2008
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "refresh", "--agent", agent], {
2009
+ cwd: process.cwd(),
2010
+ env: process.env,
2011
+ detached: true,
2012
+ stdio: "ignore",
2013
+ windowsHide: true
2014
+ });
2015
+ child.on("error", () => {
2016
+ });
2017
+ child.unref();
2018
+ }
2019
+ async function hook(agent, { silent = false } = {}) {
2020
+ const context = await usageContext(agent);
2021
+ const cached = await cachedUsage(context);
2022
+ if ((!cached || cached.ageMs >= REFRESH_INTERVAL_MS) && await canRefreshUsage(context, REFRESH_INTERVAL_MS)) {
2023
+ scheduleRefresh(agent);
2024
+ }
2025
+ hookOut(silent ? "" : cached && cached.ageMs < HOOK_SNAPSHOT_MAX_AGE_MS ? cached.text : "");
2026
+ }
1945
2027
  async function main() {
1946
2028
  try {
1947
2029
  if (mode === "refresh") {
1948
2030
  await refresh(cli.agent);
1949
- } else if (mode === "print" || mode === "print-or-refresh") {
1950
- const result = await refresh(cli.agent);
2031
+ } else if (mode === "print") {
2032
+ const result = await queryAgentProviderUsage(cli.agent);
1951
2033
  textOut(result?.text || "");
1952
2034
  } else if (mode === "hook") {
1953
- const result = await queryAgentProviderUsage(cli.agent, { maxAgeMs: snapshotTtlMs() });
1954
- hookOut(result?.text || "");
2035
+ await hook(cli.agent, { silent: cli.silent });
1955
2036
  } else {
1956
2037
  throw new Error(`unknown mode: ${mode}`);
1957
2038
  }
@@ -0,0 +1,53 @@
1
+ # Custom gateway routes
2
+
3
+ Advanced guide for [provider usage](../../README.md#provider-usage): write your own
4
+ usage probe for relays the built-in presets cannot reach (e.g. cookie-authenticated
5
+ gateways) without modifying package code.
6
+
7
+ ## Declare a route
8
+
9
+ Write a route module and list it in `providerUsage.routes` (paths resolve against
10
+ `~/.agent-tools`). Declared routes are probed first; setting `"preset"` to a route
11
+ id selects it directly.
12
+
13
+ ```jsonc
14
+ {
15
+ "providerUsage": {
16
+ "routes": [
17
+ "custom/my-gateway.mjs",
18
+ "custom/another-gateway.mjs"
19
+ ],
20
+ "myGateway": { "username": "me", "password": "..." }
21
+ }
22
+ }
23
+ ```
24
+
25
+ ## Route module API
26
+
27
+ ```js
28
+ // ~/.agent-tools/custom/my-gateway.mjs
29
+ export const meta = { id: "my-gateway" }; // optional; id defaults to the file name
30
+
31
+ export async function run(context, { requestJson, agentConfig }) {
32
+ // context: { baseUrl, key, providerName, provider, label }
33
+ const { myGateway = {} } = await agentConfig(); // the providerUsage object; custom keys welcome
34
+
35
+ const login = await fetch(`${context.baseUrl}/api/user/login`, {
36
+ method: "POST",
37
+ headers: { "content-type": "application/json" },
38
+ body: JSON.stringify({ username: myGateway.username, password: myGateway.password }),
39
+ });
40
+ const session = await login.json();
41
+
42
+ // requestJson parses JSON and throws on non-2xx responses; pass custom
43
+ // authorization or cookie headers here when needed.
44
+ const me = await requestJson(`${context.baseUrl}/api/user/self`, {
45
+ headers: { authorization: `Bearer ${session?.data?.accessToken}` },
46
+ });
47
+ return { text: `balance ¥${me?.data?.balance}` };
48
+ }
49
+ ```
50
+
51
+ `text` is a free-form string; return `{ text }` on success, throw to fall through
52
+ to the next route. Enable `providerUsage.debug` to log probe failures to
53
+ `~/.agent-tools/logs/usage-debug.log`.
@@ -0,0 +1,20 @@
1
+ # Repository structure
2
+
3
+ ```text
4
+ agent-tools/
5
+ ├── .claude-plugin/ # Claude Code/plugin ecosystem manifest.
6
+ ├── .codex-plugin/ # Codex plugin manifest.
7
+ ├── integrations/ # Installable capabilities, one directory each.
8
+ │ ├── statusline/ # Agent status line: branch, model, usage.
9
+ │ ├── usage/ # Provider balance / quota display.
10
+ │ └── vision/ # Cross-model image understanding.
11
+ ├── skills/ # Reusable Agent Skills.
12
+ │ ├── workflow/ # Workflow-oriented skills.
13
+ │ │ ├── at-commit/ # Conventional Commit message skill.
14
+ │ │ ├── at-review/ # Review changes for bugs and regressions.
15
+ │ │ └── at-simplify/ # Reduce complexity and duplication in changes.
16
+ │ └── integrations/ # Skills that integrate external systems.
17
+ │ └── at-zentao/ # ZenTao bug/task fixing workflow.
18
+ ├── docs/ # Advanced guides and contributor reference.
19
+ └── scripts/ # Install, sync, validation, and maintenance scripts.
20
+ ```
@@ -0,0 +1,47 @@
1
+ # 自定义网关路由
2
+
3
+ [Provider usage](../../README.zh-CN.md#provider-usage) 的高级指南: 为内置 preset 覆盖不到的中转(比如 cookie 认证的网关)编写自己的用量探测, 无需修改包内代码.
4
+
5
+ ## 声明路由
6
+
7
+ 编写路由模块, 并在 `providerUsage.routes` 里声明(相对 `~/.agent-tools` 解析). 声明的路由优先探测; `"preset"` 填路由 id 可直接选中.
8
+
9
+ ```jsonc
10
+ {
11
+ "providerUsage": {
12
+ "routes": [
13
+ "custom/my-gateway.mjs",
14
+ "custom/another-gateway.mjs"
15
+ ],
16
+ "myGateway": { "username": "me", "password": "..." }
17
+ }
18
+ }
19
+ ```
20
+
21
+ ## 路由模块 API
22
+
23
+ ```js
24
+ // ~/.agent-tools/custom/my-gateway.mjs
25
+ export const meta = { id: "my-gateway" }; // 可选; id 默认取文件名
26
+
27
+ export async function run(context, { requestJson, agentConfig }) {
28
+ // context: { baseUrl, key, providerName, provider, label }
29
+ const { myGateway = {} } = await agentConfig(); // providerUsage 对象, 自定义键随意加
30
+
31
+ const login = await fetch(`${context.baseUrl}/api/user/login`, {
32
+ method: "POST",
33
+ headers: { "content-type": "application/json" },
34
+ body: JSON.stringify({ username: myGateway.username, password: myGateway.password }),
35
+ });
36
+ const session = await login.json();
37
+
38
+ // requestJson 会解析 JSON, 并在非 2xx 响应时抛错; 需要时可在这里传入
39
+ // authorization、cookie 等自定义认证 header.
40
+ const me = await requestJson(`${context.baseUrl}/api/user/self`, {
41
+ headers: { authorization: `Bearer ${session?.data?.accessToken}` },
42
+ });
43
+ return { text: `balance ¥${me?.data?.balance}` };
44
+ }
45
+ ```
46
+
47
+ `text` 是自由字符串; 成功返回 `{ text }`, 抛错则回落到下一条路由. 开启 `providerUsage.debug` 后, 探测失败会记录到 `~/.agent-tools/logs/usage-debug.log`.
@@ -0,0 +1,20 @@
1
+ # 仓库结构
2
+
3
+ ```text
4
+ agent-tools/
5
+ ├── .claude-plugin/ # Claude Code/plugin 生态的 manifest.
6
+ ├── .codex-plugin/ # Codex plugin manifest.
7
+ ├── integrations/ # 可安装的 capability, 一个一目录.
8
+ │ ├── statusline/ # Agent 状态栏: 分支, 模型, 用量.
9
+ │ ├── usage/ # Provider 余额/额度显示.
10
+ │ └── vision/ # 跨模型识图.
11
+ ├── skills/ # 可复用的 Agent Skills.
12
+ │ ├── workflow/ # 工作流类 skills.
13
+ │ │ ├── at-commit/ # 生成 Conventional Commits message.
14
+ │ │ ├── at-review/ # 审查改动中的 bug 与回归风险.
15
+ │ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
16
+ │ └── integrations/ # 对接外部系统的 skills.
17
+ │ └── at-zentao/ # 禅道 bug/task 修复工作流.
18
+ ├── docs/ # 高级指南和贡献者参考.
19
+ └── scripts/ # 安装, 同步, 校验和仓库维护脚本.
20
+ ```
@@ -16,6 +16,7 @@ const LOG_PATH = path.join(AGENT_TOOLS_HOME, "logs", "usage-hook.log");
16
16
  const TIMEOUT_MS = Number(process.env.AGENT_TOOLS_USAGE_HOOK_TIMEOUT_MS || 4500);
17
17
  const MAX_LOG_BYTES = Number(process.env.AGENT_TOOLS_USAGE_HOOK_LOG_BYTES || 256 * 1024);
18
18
  const KEEP_LOG_BYTES = 128 * 1024;
19
+ const SILENT = process.argv.includes("--silent");
19
20
 
20
21
  function hookOut(message) {
21
22
  const payload = { continue: true };
@@ -64,6 +65,10 @@ function failureMessage() {
64
65
  return `API usage hook failed; see ${LOG_PATH.replace(/\\/g, "/")}`;
65
66
  }
66
67
 
68
+ function hookFailureOut() {
69
+ hookOut(SILENT ? "" : failureMessage());
70
+ }
71
+
67
72
  function parseHookJson(stdout) {
68
73
  const text = stdout.trim();
69
74
  if (!text) return { continue: true };
@@ -73,17 +78,21 @@ function parseHookJson(stdout) {
73
78
  async function runUsageScript() {
74
79
  if (!fs.existsSync(USAGE_SCRIPT)) {
75
80
  logFailure({ reason: "missing usage script", usageScript: USAGE_SCRIPT });
76
- hookOut(failureMessage());
81
+ hookFailureOut();
77
82
  return;
78
83
  }
79
84
 
80
85
  const result = await new Promise((resolve) => {
81
- const child = spawn(process.execPath, [USAGE_SCRIPT, "hook", "--agent", "codex"], {
82
- cwd: process.cwd(),
83
- env: process.env,
84
- stdio: ["ignore", "pipe", "pipe"],
85
- windowsHide: true,
86
- });
86
+ const child = spawn(
87
+ process.execPath,
88
+ [USAGE_SCRIPT, "hook", "--agent", "codex", ...(SILENT ? ["--silent"] : [])],
89
+ {
90
+ cwd: process.cwd(),
91
+ env: process.env,
92
+ stdio: ["ignore", "pipe", "pipe"],
93
+ windowsHide: true,
94
+ }
95
+ );
87
96
  let stdout = "";
88
97
  let stderr = "";
89
98
  let settled = false;
@@ -126,7 +135,7 @@ async function runUsageScript() {
126
135
  node: process.version,
127
136
  platform: `${process.platform} ${os.release()}`,
128
137
  });
129
- hookOut(failureMessage());
138
+ hookFailureOut();
130
139
  return;
131
140
  }
132
141
 
@@ -141,7 +150,7 @@ async function runUsageScript() {
141
150
  stderr: preview(result.stderr),
142
151
  usageScript: USAGE_SCRIPT,
143
152
  });
144
- hookOut(failureMessage());
153
+ hookFailureOut();
145
154
  }
146
155
  }
147
156
 
@@ -153,5 +162,5 @@ try {
153
162
  error: error?.stack || error?.message || String(error),
154
163
  usageScript: USAGE_SCRIPT,
155
164
  });
156
- hookOut(failureMessage());
165
+ hookFailureOut();
157
166
  }