@kairyou/agent-tools 0.1.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.
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: at-review
3
+ description: "Review code changes for bugs, regressions, convention violations, and high-value cleanup opportunities. Use for diffs, commit ranges, PRs, paths, staged changes, or working-tree changes."
4
+ ---
5
+
6
+ # Code Review
7
+
8
+ `high effort → 3+5 angles × 6 candidates → 1-vote verify (recall-biased) → ≤10 findings`
9
+
10
+ You are reviewing for **recall** at high effort: catch every real bug a careful reviewer would catch in one sitting. At this level, catching real bugs matters more than avoiding false positives. Err on the side of surfacing.
11
+
12
+ ## Phase 0 — Gather the diff
13
+
14
+ Run `git diff "@{upstream}...HEAD"` (or `git diff main...HEAD` / `git diff HEAD~1` if there's no upstream) to get the unified diff under review. If there are uncommitted changes, or the range diff is empty, also run `git diff HEAD` and include the working-tree changes in scope — the review often runs before the commit. If a PR number, branch name, or file path was passed as an argument, review that target instead. Treat this diff as the review scope.
15
+
16
+ ## Phase 1 — Find candidates (3 correctness angles + 3 cleanup angles + 1 altitude angle + 1 conventions angle, up to 6 each)
17
+
18
+ Run **8 independent finder angles** using multi-agent capabilities. Each surfaces **up to 6 candidate findings** with `file`, `line`, a one-line `summary`, and a concrete `failure_scenario`.
19
+
20
+ ### Angle A — line-by-line diff scan
21
+
22
+ Read every hunk in the diff, line by line. Then Read the enclosing function for each hunk — bugs in unchanged lines of a touched function are in scope (the PR re-exposes or fails to fix them). For every line ask: what input, state, timing, or platform makes this line wrong? Look for inverted/wrong conditions, off-by-one, null/undefined deref, missing `await`, falsy-zero checks, wrong-variable copy-paste, error swallowed in catch, unescaped regex metachars.
23
+
24
+ ### Angle B — removed-behavior auditor
25
+
26
+ For every line the diff DELETES or replaces, name the invariant or behavior it enforced, then search the new code for where that invariant is re-established. If you can't find it, that's a candidate: a removed guard, a dropped error path, a narrowed validation, a deleted test that was covering a real case.
27
+
28
+ ### Angle C — cross-file tracer
29
+
30
+ For each function the diff changes, find its callers (Grep for the symbol) and check whether the change breaks any call site: a new precondition, a changed return shape, a new exception, a timing/ordering dependency. Also check callees: does a parallel change in the same PR make a call unsafe?
31
+
32
+ ### Reuse
33
+
34
+ The angles above hunt for bugs; this one and the next two hunt for cleanup in the changed code. Flag new code that re-implements something the codebase already has — Grep shared/utility modules and files adjacent to the change, and name the existing helper to call instead.
35
+
36
+ ### Simplification
37
+
38
+ Flag unnecessary complexity the diff adds: redundant or derivable state, copy-paste with slight variation, deep nesting, dead code left behind. Name the simpler form that does the same job.
39
+
40
+ ### Efficiency
41
+
42
+ Flag wasted work the diff introduces: redundant computation or repeated I/O, independent operations run sequentially, blocking work added to startup or hot paths. Also flag long-lived objects built from closures or captured environments — they keep the entire enclosing scope alive for the object's lifetime (a memory leak when that scope holds large values); prefer a class/struct that copies only the fields it needs. Name the cheaper alternative.
43
+
44
+ ### Altitude
45
+
46
+ Check that each change is implemented at the right depth, not as a fragile bandaid. Special cases layered on shared infrastructure are a sign the fix isn't deep enough — prefer generalizing the underlying mechanism over adding special cases.
47
+
48
+ ### Conventions (project instructions)
49
+
50
+ Find the instruction files that govern the changed code: user-level instructions for the current agent, the repo-root AGENTS.md or CLAUDE.md, plus any AGENTS.md, CLAUDE.md, or CLAUDE.local.md in a directory that is an ancestor of a changed file (a directory's instruction file only applies to files at or below it). Read each one that exists, then check the diff for clear violations of the rules they state.
51
+
52
+ Only flag a violation when you can quote the exact rule and the exact line that breaks it — no style preferences, no vague "spirit of the doc" inferences. In the finding, name the instruction file path and quote the rule so the report can cite it. If no instruction file applies, return nothing for this angle.
53
+
54
+ Cleanup, altitude, and conventions candidates use the same `file`/`line`/`summary` shape; in `failure_scenario`, state the concrete cost (what is duplicated, wasted, harder to maintain, or which project instruction is broken) instead of a crash. Correctness bugs always outrank cleanup, altitude, and conventions findings when the output cap forces a cut.
55
+
56
+ Pass every candidate with a nameable failure scenario through — finders that silently drop half-believed candidates bypass the verify step and are the dominant cause of misses.
57
+
58
+ ## Phase 2 — Verify (1-vote, recall-biased)
59
+
60
+ Dedup near-duplicates (same defect, same location, same reason → keep one). For each remaining candidate, run **one verifier** using multi-agent capabilities: give it the diff, the relevant file(s), and the candidate; it returns exactly one of **CONFIRMED / PLAUSIBLE / REFUTED**.
61
+
62
+ **PLAUSIBLE by default** — do not refute a candidate for being "speculative" or "depends on runtime state" when the state is realistic: concurrency races, nil/undefined on a rare-but-reachable path (error handler, cold cache, missing optional field), falsy-zero treated as missing, off-by-one on a boundary the code does not exclude, retry storms / partial failures, regex/allowlist that lost an anchor. These are PLAUSIBLE.
63
+
64
+ **REFUTED** only when constructible from the code: factually wrong (quote the actual line); provably impossible (type/constant/invariant — show it); already handled in this diff (cite the guard); or pure style with no observable effect.
65
+
66
+ Keep **CONFIRMED and PLAUSIBLE**. Drop REFUTED.
67
+
68
+ ## Output
69
+
70
+ Return findings as a JSON array of at most 10 objects:
71
+
72
+ ```json
73
+ [
74
+ {
75
+ "file": "path/to/file.ext",
76
+ "line": 123,
77
+ "summary": "one-sentence statement of the bug",
78
+ "failure_scenario": "concrete inputs/state → wrong output/crash"
79
+ }
80
+ ]
81
+ ```
82
+
83
+ Ranked most-severe first. If more than 10 survive, keep the 10 most severe. If nothing survives verification, return `[]`.
84
+
85
+ ## Applying fixes
86
+
87
+ If the user asked to fix, apply the findings to the working tree instead of stopping at the report: fix each one directly — correctness bugs and reuse/simplification/efficiency cleanups alike. Skip any finding whose fix would change intended behavior, require changes well outside the reviewed diff, or that you judge to be a false positive — note the skip rather than arguing with it. Finish with a brief summary of what was fixed and what was skipped.
88
+
89
+ If the user did NOT ask to fix, stop at the report.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: at-simplify
3
+ description: "Refactor changed code to reduce duplication, complexity, and wasted work. Use for diffs, commit ranges, PRs, paths, staged changes, or working-tree changes."
4
+ ---
5
+
6
+ # Simplify
7
+
8
+ `at-simplify → 4 cleanup agents in parallel → apply the fixes`
9
+
10
+ You are improving the quality of the changed code, not hunting for bugs. Review
11
+ it for reuse, simplification, efficiency, and altitude issues, then fix what you
12
+ find. Do not look for correctness bugs — that is what `at-review` is for.
13
+
14
+ ## Phase 0 — Gather the diff
15
+
16
+ Run `git diff "@{upstream}...HEAD"` (or `git diff main...HEAD` / `git diff HEAD~1`
17
+ if there's no upstream) to get the unified diff under review. If there are
18
+ uncommitted changes, or the range diff is empty, also run `git diff HEAD` and
19
+ include the working-tree changes in scope — the review often runs before the
20
+ commit. If a PR number, branch name, or file path was passed as an argument,
21
+ review that target instead. Treat this diff as the review scope.
22
+
23
+ ## Phase 1 — Review (4 cleanup agents in parallel)
24
+
25
+ Launch **4 independent review agents** using multi-agent capabilities, all in a
26
+ single message so they run concurrently. Pass each agent the diff and one of
27
+ the four angles below. Each returns its findings with `file`, `line`, a
28
+ one-line `summary`, and the concrete cost (what is duplicated, wasted, or
29
+ harder to maintain).
30
+
31
+ ### Reuse
32
+
33
+ Flag new code that re-implements something the codebase
34
+ already has — Grep shared/utility modules and files adjacent to the change,
35
+ and name the existing helper to call instead.
36
+
37
+ ### Simplification
38
+
39
+ Flag unnecessary complexity the diff adds: redundant or derivable state,
40
+ copy-paste with slight variation, deep nesting, dead code left behind. Name
41
+ the simpler form that does the same job.
42
+
43
+ ### Efficiency
44
+
45
+ Flag wasted work the diff introduces: redundant computation or repeated I/O,
46
+ independent operations run sequentially, blocking work added to startup or
47
+ hot paths. Also flag long-lived objects built from closures or captured
48
+ environments — they keep the entire enclosing scope alive for the object's
49
+ lifetime (a memory leak when that scope holds large values); prefer a
50
+ class/struct that copies only the fields it needs. Name the cheaper
51
+ alternative.
52
+
53
+ ### Altitude
54
+
55
+ Check that each change is implemented at the right depth, not as a fragile
56
+ bandaid. Special cases layered on shared infrastructure are a sign the fix
57
+ isn't deep enough — prefer generalizing the underlying mechanism over adding
58
+ special cases.
59
+
60
+ ## Phase 2 — Apply the fixes
61
+
62
+ Wait for all four agents to complete, dedup findings that point at the same
63
+ line or mechanism, and fix each remaining one directly. Skip any finding whose
64
+ fix would change intended behavior, require changes well outside the reviewed
65
+ diff, or that you judge to be a false positive — note the skip rather than
66
+ arguing with it. Finish with a brief summary of what was fixed and what was
67
+ skipped (or confirm the code was already clean).
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code statusLine script (agent-tools).
3
+ // Reads session JSON from stdin and prints one compact status line.
4
+ //
5
+ // Default:
6
+ // ⎇ main | Opus 4.8 | 5h 7% ⟳2h54m | w 41% ⟳3d1h
7
+ //
8
+ // Customize with either:
9
+ // node statusline.mjs --fields branch,model,fiveHour,week
10
+ // AGENT_TOOLS_STATUSLINE_FIELDS=branch,model,context
11
+ // ~/.agent-tools/config.jsonc
12
+
13
+ import { execFileSync, spawn } from "node:child_process";
14
+ import fs from "node:fs";
15
+ import { basename, dirname, join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
19
+ const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(SCRIPT_DIR, "..", "..");
20
+ const DEFAULT_CONFIG_FILE = join(AGENT_TOOLS_HOME, "config.jsonc");
21
+ const SNAPSHOT_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
22
+ const REFRESH_STATE_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-refresh-state.json");
23
+ const USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "lib", "usage.mjs");
24
+ const DEFAULT_SNAPSHOT_TTL_MS = 60_000;
25
+ const DEFAULT_REFRESH_COOLDOWN_MS = 30_000;
26
+ const DEFAULT_FAILURE_BACKOFF_MS = 120_000;
27
+
28
+ const DEFAULT_CONFIG = {
29
+ fields: ["branch", "model", "fiveHour", "week"],
30
+ separator: " | ",
31
+ symbols: {
32
+ branch: "⎇",
33
+ reset: "⟳",
34
+ empty: "–",
35
+ fiveHour: "5h",
36
+ week: "w",
37
+ context: "ctx",
38
+ },
39
+ };
40
+
41
+ const FIELD_ALIASES = {
42
+ cwd: "directory",
43
+ dir: "directory",
44
+ five: "fiveHour",
45
+ five_hour: "fiveHour",
46
+ "5h": "fiveHour",
47
+ sevenDay: "week",
48
+ seven_day: "week",
49
+ "7d": "week",
50
+ weekly: "week",
51
+ ctx: "context",
52
+ };
53
+
54
+ async function readStdin() {
55
+ const chunks = [];
56
+ for await (const chunk of process.stdin) chunks.push(chunk);
57
+ return Buffer.concat(chunks).toString("utf8");
58
+ }
59
+
60
+ function readJsonFile(file) {
61
+ try {
62
+ if (!fs.existsSync(file)) return {};
63
+ const raw = stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, ""));
64
+ return raw.trim() ? JSON.parse(raw) : {};
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
70
+ function stripJsonComments(input) {
71
+ let out = "";
72
+ let inString = false;
73
+ let escaped = false;
74
+ for (let i = 0; i < input.length; i++) {
75
+ const ch = input[i];
76
+ const next = input[i + 1];
77
+ if (inString) {
78
+ out += ch;
79
+ escaped = ch === "\\" ? !escaped : false;
80
+ if (ch === "\"" && !escaped) inString = false;
81
+ continue;
82
+ }
83
+ if (ch === "\"") {
84
+ inString = true;
85
+ out += ch;
86
+ continue;
87
+ }
88
+ if (ch === "/" && next === "/") {
89
+ while (i < input.length && input[i] !== "\n") i++;
90
+ out += "\n";
91
+ continue;
92
+ }
93
+ if (ch === "/" && next === "*") {
94
+ i += 2;
95
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
96
+ i++;
97
+ continue;
98
+ }
99
+ out += ch;
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function parseArgs(argv) {
105
+ const opts = {};
106
+ for (let i = 0; i < argv.length; i++) {
107
+ const arg = argv[i];
108
+ if (arg === "--fields" && argv[i + 1]) {
109
+ opts.fields = argv[++i];
110
+ } else if (arg.startsWith("--fields=")) {
111
+ opts.fields = arg.slice("--fields=".length);
112
+ } else if (arg === "--separator" && argv[i + 1]) {
113
+ opts.separator = argv[++i];
114
+ } else if (arg.startsWith("--separator=")) {
115
+ opts.separator = arg.slice("--separator=".length);
116
+ }
117
+ }
118
+ return opts;
119
+ }
120
+
121
+ function splitFields(value) {
122
+ if (Array.isArray(value)) return value;
123
+ if (typeof value !== "string") return null;
124
+ return value
125
+ .split(/[,\s]+/)
126
+ .map((part) => part.trim())
127
+ .filter(Boolean);
128
+ }
129
+
130
+ function normalizeField(field) {
131
+ return FIELD_ALIASES[field] || field;
132
+ }
133
+
134
+ function mergeConfig(cli) {
135
+ const rootConfig = readJsonFile(process.env.AGENT_TOOLS_CONFIG || DEFAULT_CONFIG_FILE);
136
+ const fileConfig = rootConfig.statusline || {};
137
+ const envFields = process.env.AGENT_TOOLS_STATUSLINE_FIELDS;
138
+ const envSeparator = process.env.AGENT_TOOLS_STATUSLINE_SEPARATOR;
139
+
140
+ const config = {
141
+ ...DEFAULT_CONFIG,
142
+ ...fileConfig,
143
+ symbols: { ...DEFAULT_CONFIG.symbols, ...(fileConfig.symbols || {}) },
144
+ };
145
+
146
+ const fields =
147
+ splitFields(cli.fields) ||
148
+ splitFields(envFields) ||
149
+ splitFields(fileConfig.fields) ||
150
+ DEFAULT_CONFIG.fields;
151
+
152
+ config.fields = fields.map(normalizeField);
153
+ if (typeof envSeparator === "string") config.separator = envSeparator;
154
+ if (typeof cli.separator === "string") config.separator = cli.separator;
155
+ return config;
156
+ }
157
+
158
+ function gitBranch(cwd) {
159
+ try {
160
+ const out = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
161
+ cwd,
162
+ stdio: ["ignore", "pipe", "ignore"],
163
+ encoding: "utf8",
164
+ }).trim();
165
+ return out && out !== "HEAD" ? out : "";
166
+ } catch {
167
+ return "";
168
+ }
169
+ }
170
+
171
+ function secondsUntil(unixSeconds) {
172
+ if (!unixSeconds) return null;
173
+ const seconds = Number(unixSeconds) - Math.floor(Date.now() / 1000);
174
+ return Number.isFinite(seconds) ? Math.max(0, seconds) : null;
175
+ }
176
+
177
+ function compactDuration(totalSeconds) {
178
+ if (totalSeconds == null) return "";
179
+ if (totalSeconds <= 0) return "0m";
180
+ let seconds = totalSeconds;
181
+ const days = Math.floor(seconds / 86400);
182
+ seconds %= 86400;
183
+ const hours = Math.floor(seconds / 3600);
184
+ seconds %= 3600;
185
+ const minutes = Math.floor(seconds / 60);
186
+ if (days) return `${days}d${hours}h`;
187
+ if (hours) return `${hours}h${minutes}m`;
188
+ return `${minutes}m`;
189
+ }
190
+
191
+ function usageWindow(window, config) {
192
+ if (!window || typeof window.used_percentage !== "number") {
193
+ return "";
194
+ }
195
+ const pct = `${Math.round(window.used_percentage)}%`;
196
+ const left = compactDuration(secondsUntil(window.resets_at));
197
+ return left ? `${pct} ${config.symbols.reset}${left}` : pct;
198
+ }
199
+
200
+ function showMissingUsageWindow() {
201
+ const baseUrl = activeRelayBaseUrl();
202
+ return !baseUrl || isOfficialBaseUrl(baseUrl);
203
+ }
204
+
205
+ function shortModelName(name) {
206
+ if (!name) return "";
207
+ return String(name)
208
+ .replace(/^Claude\s+/i, "")
209
+ .replace(/\s*\[1m\]\s*$/i, "")
210
+ .trim();
211
+ }
212
+
213
+ function renderField(field, data, config) {
214
+ const dir = data?.workspace?.current_dir || data?.cwd || process.cwd() || "";
215
+ const projectDir = data?.workspace?.project_dir || dir;
216
+ switch (field) {
217
+ case "branch": {
218
+ const branch = gitBranch(projectDir);
219
+ return branch ? `${config.symbols.branch} ${branch}` : "";
220
+ }
221
+ case "model":
222
+ return shortModelName(data?.model?.display_name || data?.model?.id || "");
223
+ case "fiveHour": {
224
+ const value = usageWindow(data?.rate_limits?.five_hour, config);
225
+ return value || showMissingUsageWindow()
226
+ ? `${config.symbols.fiveHour} ${value || config.symbols.empty}`
227
+ : "";
228
+ }
229
+ case "week": {
230
+ const value = usageWindow(data?.rate_limits?.seven_day, config);
231
+ return value || showMissingUsageWindow()
232
+ ? `${config.symbols.week} ${value || config.symbols.empty}`
233
+ : "";
234
+ }
235
+ case "context": {
236
+ const pct = data?.context_window?.used_percentage;
237
+ return typeof pct === "number" ? `${config.symbols.context} ${Math.round(pct)}%` : "";
238
+ }
239
+ case "directory":
240
+ return dir ? basename(dir) : "";
241
+ default:
242
+ return "";
243
+ }
244
+ }
245
+
246
+ function numberFromEnv(name, fallback) {
247
+ const value = Number(process.env[name]);
248
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
249
+ }
250
+
251
+ function cleanBaseUrl(baseUrl) {
252
+ return String(baseUrl || "").replace(/\/+$/, "");
253
+ }
254
+
255
+ function isOfficialBaseUrl(baseUrl) {
256
+ if (!baseUrl) return true;
257
+ const clean = cleanBaseUrl(baseUrl);
258
+ return [
259
+ "https://api.anthropic.com",
260
+ "https://api.anthropic.com/v1",
261
+ "https://api.openai.com",
262
+ "https://api.openai.com/v1",
263
+ ].includes(clean);
264
+ }
265
+
266
+ function usageRouteCacheKey(baseUrl) {
267
+ try {
268
+ const url = new URL(cleanBaseUrl(baseUrl));
269
+ url.hash = "";
270
+ url.search = "";
271
+ url.pathname = url.pathname
272
+ .replace(/\/+$/, "")
273
+ .replace(/\/api\/v1$/i, "")
274
+ .replace(/\/v1$/i, "");
275
+ return url.toString().replace(/\/$/, "");
276
+ } catch {
277
+ return cleanBaseUrl(baseUrl).endsWith("/v1") ? cleanBaseUrl(baseUrl).slice(0, -3) : cleanBaseUrl(baseUrl);
278
+ }
279
+ }
280
+
281
+ function readJsonFileRaw(file) {
282
+ try {
283
+ if (!fs.existsSync(file)) return {};
284
+ const raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "");
285
+ return raw.trim() ? JSON.parse(raw) : {};
286
+ } catch {
287
+ return {};
288
+ }
289
+ }
290
+
291
+ function activeRelayBaseUrl() {
292
+ return process.env.PROVIDER_USAGE_BASE_URL || process.env.ANTHROPIC_BASE_URL || "";
293
+ }
294
+
295
+ function hasClaudeUsageToken() {
296
+ return Boolean(
297
+ process.env.PROVIDER_USAGE_API_KEY ||
298
+ process.env.ANTHROPIC_AUTH_TOKEN ||
299
+ process.env.ANTHROPIC_API_KEY
300
+ );
301
+ }
302
+
303
+ function snapshotForBaseUrl(baseUrl) {
304
+ const snapshot = readJsonFileRaw(SNAPSHOT_FILE);
305
+ const key = usageRouteCacheKey(baseUrl);
306
+ const item = snapshot?.items?.[key];
307
+ return item?.text ? item : null;
308
+ }
309
+
310
+ function refreshStateForBaseUrl(baseUrl) {
311
+ const state = readJsonFileRaw(REFRESH_STATE_FILE);
312
+ return state?.items?.[usageRouteCacheKey(baseUrl)] || {};
313
+ }
314
+
315
+ function ageMs(isoDate) {
316
+ const time = Date.parse(isoDate || "");
317
+ return Number.isFinite(time) ? Date.now() - time : Number.POSITIVE_INFINITY;
318
+ }
319
+
320
+ function writeRefreshAttempt(baseUrl) {
321
+ try {
322
+ const state = readJsonFileRaw(REFRESH_STATE_FILE);
323
+ const key = usageRouteCacheKey(baseUrl);
324
+ state.version = 1;
325
+ state.items = state.items && typeof state.items === "object" ? state.items : {};
326
+ state.items[key] = {
327
+ ...(state.items[key] || {}),
328
+ baseUrl,
329
+ lastAttemptAt: new Date().toISOString(),
330
+ };
331
+ fs.mkdirSync(dirname(REFRESH_STATE_FILE), { recursive: true });
332
+ fs.writeFileSync(REFRESH_STATE_FILE, `${JSON.stringify(state, null, 2)}\n`);
333
+ } catch {
334
+ // Statusline must stay non-blocking and fail-open.
335
+ }
336
+ }
337
+
338
+ function shouldRefreshUsage(baseUrl, snapshot) {
339
+ if (process.env.AGENT_TOOLS_USAGE_REFRESH === "0") return false;
340
+ if (!baseUrl || isOfficialBaseUrl(baseUrl)) return false;
341
+ if (!hasClaudeUsageToken()) return false;
342
+ if (!fs.existsSync(USAGE_RUNTIME)) return false;
343
+
344
+ const ttlMs = numberFromEnv("AGENT_TOOLS_USAGE_SNAPSHOT_TTL_MS", DEFAULT_SNAPSHOT_TTL_MS);
345
+ const cooldownMs = numberFromEnv("AGENT_TOOLS_USAGE_REFRESH_COOLDOWN_MS", DEFAULT_REFRESH_COOLDOWN_MS);
346
+ const failureBackoffMs = numberFromEnv("AGENT_TOOLS_USAGE_FAILURE_BACKOFF_MS", DEFAULT_FAILURE_BACKOFF_MS);
347
+ const state = refreshStateForBaseUrl(baseUrl);
348
+
349
+ if (ageMs(state.lastAttemptAt) < cooldownMs) return false;
350
+ if (state.lastError && ageMs(state.lastFailureAt) < failureBackoffMs) return false;
351
+ return !snapshot || ageMs(snapshot.updatedAt) >= ttlMs;
352
+ }
353
+
354
+ function refreshUsageInBackground(baseUrl) {
355
+ if (!shouldRefreshUsage(baseUrl, snapshotForBaseUrl(baseUrl))) return;
356
+ writeRefreshAttempt(baseUrl);
357
+ try {
358
+ const child = spawn(process.execPath, [USAGE_RUNTIME, "refresh", "--agent", "claude"], {
359
+ detached: true,
360
+ stdio: "ignore",
361
+ env: process.env,
362
+ windowsHide: true,
363
+ });
364
+ child.unref();
365
+ } catch {
366
+ // Statusline must never surface provider usage refresh errors.
367
+ }
368
+ }
369
+
370
+ function providerUsageStatus() {
371
+ const baseUrl = activeRelayBaseUrl();
372
+ if (!baseUrl || isOfficialBaseUrl(baseUrl)) return "";
373
+ const snapshot = snapshotForBaseUrl(baseUrl);
374
+ if (shouldRefreshUsage(baseUrl, snapshot)) refreshUsageInBackground(baseUrl);
375
+ return snapshot?.text || "";
376
+ }
377
+
378
+ function render(data, config) {
379
+ const fields = config.fields
380
+ .map((field) => renderField(field, data, config))
381
+ .filter(Boolean);
382
+ const providerUsage = providerUsageStatus();
383
+ if (providerUsage) fields.push(providerUsage);
384
+ return fields.join(config.separator);
385
+ }
386
+
387
+ async function main() {
388
+ const config = mergeConfig(parseArgs(process.argv.slice(2)));
389
+ let data = {};
390
+ try {
391
+ const raw = await readStdin();
392
+ data = raw.trim() ? JSON.parse(raw) : {};
393
+ } catch {
394
+ data = {};
395
+ }
396
+ process.stdout.write(render(data, config));
397
+ }
398
+
399
+ main();
@@ -0,0 +1 @@
1
+