@kairyou/agent-tools 0.3.0 → 0.4.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 CHANGED
@@ -1,28 +1,10 @@
1
1
  # Agent Tools
2
2
 
3
- Reusable Agent Skills, plus runtime integrations for Codex, Claude Code, and opencode.
3
+ Reusable Agent Skills and installable integrations — statusline, provider usage, and vision — for Codex, Claude Code, and opencode.
4
4
 
5
- [中文](README.zh-CN.md)
6
-
7
- ## Repository Structure
5
+ Requires Node.js >= 22.
8
6
 
9
- ```text
10
- agent-tools/
11
- ├── .claude-plugin/ # Claude Code/plugin ecosystem manifest.
12
- ├── .codex-plugin/ # Codex plugin manifest.
13
- ├── integrations/ # Installable capabilities, one directory each; files named by agent + form.
14
- │ ├── statusline/ # Claude Code command-backed statusLine script.
15
- │ ├── usage/ # Provider usage (query core + codex hook, opencode plugins, CLI, at-usage skill template).
16
- │ └── vision/ # Cross-model image understanding (inspect_image MCP server + at-vision skill).
17
- ├── skills/ # Reusable Agent Skills for CLI discovery and plugin manifests.
18
- │ ├── workflow/ # Workflow-oriented skills.
19
- │ │ ├── at-commit/ # Conventional Commit message skill.
20
- │ │ ├── at-review/ # Review changes for bugs and regressions.
21
- │ │ └── at-simplify/ # Reduce complexity and duplication in changes.
22
- │ └── integrations/ # Skills that integrate external systems.
23
- │ └── at-zentao/ # ZenTao bug/task fixing workflow.
24
- └── scripts/ # Install, sync, validation, and maintenance scripts.
25
- ```
7
+ [中文](README.zh-CN.md)
26
8
 
27
9
  ## Skills
28
10
 
@@ -91,8 +73,10 @@ Runtime capabilities, installed per agent:
91
73
  npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
92
74
  ```
93
75
 
94
- `--dry-run` previews, `--uninstall` removes, and re-running the install command
95
- updates.
76
+ `--dry-run` previews, `--uninstall` unwires the integration from the agent, and
77
+ re-running the install command updates. The installer only touches config
78
+ entries it wrote itself; shared files under `~/.agent-tools` are kept on
79
+ uninstall.
96
80
 
97
81
  | Capability | Claude Code | Codex | OpenCode |
98
82
  | --- | --- | --- | --- |
@@ -122,7 +106,11 @@ it preserves top-of-file comments and existing values.
122
106
 
123
107
  ### Provider usage
124
108
 
125
- Shows the active API provider's balance / quota inside each agent.
109
+ For API relay / gateway setups: shows the relay's balance / quota inside the
110
+ agent, so when you pay per use or have plan limits you always know how much you
111
+ have spent and how much is left — without opening the gateway console.
112
+ Works with compatible Sub2API-like,
113
+ NewAPI/OneAPI/OneHub/DoneHub/Veloera/AnyRouter-like, and OpenRouter gateways.
126
114
 
127
115
  ```bash
128
116
  npx -y @kairyou/agent-tools@latest usage -a claude
@@ -134,16 +122,35 @@ npx -y @kairyou/agent-tools@latest usage -a opencode
134
122
  `/at-usage` to show the current usage in the conversation.
135
123
  - **Codex** — adds a hook to `UserPromptSubmit` and `Stop` in `~/.codex/hooks.json`
136
124
  and the `at-usage` skill to `~/.agents/skills`. Run `/hooks` inside Codex once
137
- to approve it. Hook output only appears in the Codex CLI; in clients that do
138
- not show it (e.g. Paseo), invoke `$at-usage`.
125
+ to approve it. The Codex CLI displays hook output; some clients (e.g. Paseo)
126
+ currently do not invoke `$at-usage` there.
139
127
  - **OpenCode** — adds server and TUI plugins: usage refreshes when the session
140
128
  goes idle and shows as a toast, and `/at-usage` shows the latest cached value.
141
129
  Restart opencode after installing or updating.
142
130
 
131
+ The relay endpoint is auto-discovered — Codex: the active provider's `base_url`
132
+ and key from `~/.codex/config.toml` / `auth.json`; Claude Code:
133
+ `ANTHROPIC_BASE_URL` plus `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY`.
134
+ Official (non-relay) endpoints are skipped. If it reports
135
+ `Provider usage is unavailable.`, point it at the relay explicitly —
136
+ `PROVIDER_USAGE_BASE_URL` / `PROVIDER_USAGE_API_KEY` env vars override the
137
+ endpoint and key — and tune `providerUsage` in `~/.agent-tools/config.jsonc`:
138
+
139
+ ```jsonc
140
+ {
141
+ "providerUsage": {
142
+ "preset": "auto", // sub2api | new-api | veloera | anyrouter | openrouter | ...
143
+ "userId": "", // some NewAPI/Veloera panels require your panel user id
144
+ "days": 30, // spend window for the "30d" field (max 90)
145
+ "debug": false // true: log probes to ~/.agent-tools/logs/usage-debug.log
146
+ }
147
+ }
148
+ ```
149
+
143
150
  Output examples:
144
151
 
145
152
  ```text
146
- # Subscription / plan quota.
153
+ # Relay plan quota.
147
154
  API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
148
155
 
149
156
  # Wallet balance.
@@ -153,11 +160,6 @@ API | balance $362 | today $61.7 | 30d $566
153
160
  Fields: `D/W/M` are daily/weekly/monthly spend against plan limits; `Exp` is
154
161
  the plan expiry; `balance` is wallet credit; `today` and `30d` are API spend.
155
162
 
156
- #### Supported gateways
157
-
158
- Balance, quota, and plan usage queries support compatible Sub2API-like,
159
- NewAPI/OneAPI/OneHub/DoneHub/Veloera/AnyRouter-like, and OpenRouter gateways.
160
-
161
163
  ### Vision (cross-model image understanding)
162
164
 
163
165
  Lets a main model that cannot see images ask a multimodal model specific questions about an image (local path or http(s) URL) and reason on from the answers. Typical uses: reading error screenshots, implementing UI from design mockups, locating the glitch in a bug-report screenshot. One installer capability bundling three parts: the `inspect_image` MCP stdio server, the `at-vision` policy skill, and a human diagnostic CLI.
@@ -185,8 +187,8 @@ skill into the agent's skills directory.
185
187
  "baseUrl": "https://gateway.example.com/v1", // anthropic-compatible: gateway root, /v1/messages is appended
186
188
  "model": "internal-vlm",
187
189
  "apiKey": { "env": "OPENAI_API_KEY" } // reuse an existing env var, or the key itself
188
- // optional: "timeoutMs": 30000, "maxImageBytes": 20971520, "maxOutputTokens": 8192,
189
- // "maxConcurrentRequests": 2, "maxRequestsPerMinute": 30
190
+ // , "timeoutMs": 30000, "maxImageBytes": 20971520, "maxOutputTokens": 8192
191
+ // , "maxConcurrentRequests": 2, "maxRequestsPerMinute": 30
190
192
  }
191
193
  }
192
194
  ```
@@ -214,6 +216,26 @@ To run directly from the repository, replace the npm package name with
214
216
  npx -y github:kairyou/agent-tools usage -a codex
215
217
  ```
216
218
 
219
+ ## Repository Structure
220
+
221
+ ```text
222
+ agent-tools/
223
+ ├── .claude-plugin/ # Claude Code/plugin ecosystem manifest.
224
+ ├── .codex-plugin/ # Codex plugin manifest.
225
+ ├── integrations/ # Installable capabilities, one directory each.
226
+ │ ├── statusline/ # Agent status line: branch, model, usage.
227
+ │ ├── usage/ # Provider balance / quota display.
228
+ │ └── vision/ # Cross-model image understanding.
229
+ ├── skills/ # Reusable Agent Skills.
230
+ │ ├── workflow/ # Workflow-oriented skills.
231
+ │ │ ├── at-commit/ # Conventional Commit message skill.
232
+ │ │ ├── at-review/ # Review changes for bugs and regressions.
233
+ │ │ └── at-simplify/ # Reduce complexity and duplication in changes.
234
+ │ └── integrations/ # Skills that integrate external systems.
235
+ │ └── at-zentao/ # ZenTao bug/task fixing workflow.
236
+ └── scripts/ # Install, sync, validation, and maintenance scripts.
237
+ ```
238
+
217
239
  ## FAQ
218
240
 
219
241
  ### Why does global installation fail for PromptScript?
@@ -227,8 +249,3 @@ other agents and can be ignored. See [`skills` issue #1352](https://github.com/v
227
249
  - [OpenCommit](https://github.com/di-sukharev/opencommit)
228
250
  - [GitLens](https://github.com/gitkraken/vscode-gitlens)
229
251
  - [claude-code-system-prompts](https://github.com/Piebald-AI/claude-code-system-prompts)
230
-
231
- ## Notes
232
-
233
- - The installer marks and removes only the config entries it owns.
234
- - Run local checks with `npm test`.
package/README.zh-CN.md CHANGED
@@ -1,26 +1,8 @@
1
1
  # Agent Tools
2
2
 
3
- 可复用的 Agent Skills, 以及面向 CodexClaude Code 与 opencode runtime integrations.
3
+ 可复用的 Agent Skills, 以及面向 Codex, Claude Code 与 opencode 的可安装 integrations: statusline, provider usage 与跨模型识图.
4
4
 
5
- ## 仓库结构
6
-
7
- ```text
8
- agent-tools/
9
- ├── .claude-plugin/ # Claude Code/plugin 生态的 manifest。
10
- ├── .codex-plugin/ # Codex plugin manifest。
11
- ├── integrations/ # 可安装的 capability, 一个一目录; 文件按 agent + 形态命名.
12
- │ ├── statusline/ # Claude Code command-backed statusLine 脚本.
13
- │ ├── usage/ # Provider usage (查询核心 + codex hook, opencode plugins, CLI, at-usage skill 模板).
14
- │ └── vision/ # 跨模型识图(inspect_image MCP server + 内置 at-vision skill)。
15
- ├── skills/ # 可复用 Agent Skills,供 CLI 扫描和 plugin manifest 声明。
16
- │ ├── workflow/ # 工作流类 skills。
17
- │ │ ├── at-commit/ # 生成 Conventional Commits message.
18
- │ │ ├── at-review/ # 审查改动中的 bug 与回归风险.
19
- │ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
20
- │ └── integrations/ # 对接外部系统的 skills。
21
- │ └── at-zentao/ # 禅道 bug/task 修复工作流.
22
- └── scripts/ # 安装、同步、校验和仓库维护脚本。
23
- ```
5
+ 需要 Node.js >= 22.
24
6
 
25
7
  ## Skills
26
8
 
@@ -89,7 +71,8 @@ Runtime capability, 按 agent 选装:
89
71
  npx -y @kairyou/agent-tools@latest <capability> -a <agent...>
90
72
  ```
91
73
 
92
- `--dry-run` 预览, `--uninstall` 卸载, 重新执行安装命令即为更新.
74
+ `--dry-run` 预览, `--uninstall` 解除对应集成, 重新执行安装命令即为更新.
75
+ 安装器只改动自己写入的配置项; 卸载会保留 `~/.agent-tools` 下的共享文件.
93
76
 
94
77
  | Capability | Claude Code | Codex | OpenCode |
95
78
  | --- | --- | --- | --- |
@@ -117,7 +100,10 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
117
100
 
118
101
  ### Provider usage
119
102
 
120
- 在各 agent 内显示当前 API provider 的余额/额度.
103
+ 面向使用 API 中转的场景: agent 内直接显示中转网关的余额/额度, 按量付费或
104
+ 有套餐限额时, 随时知道花了多少, 还剩多少, 不用切出去登录网关后台.
105
+ 支持兼容 Sub2API-like, NewAPI/OneAPI/OneHub/DoneHub/Veloera/AnyRouter-like
106
+ 与 OpenRouter 的网关.
121
107
 
122
108
  ```bash
123
109
  npx -y @kairyou/agent-tools@latest usage -a claude
@@ -129,15 +115,33 @@ npx -y @kairyou/agent-tools@latest usage -a opencode
129
115
  `/at-usage` 即可在对话中显示当前用量.
130
116
  - **Codex** — 把 hook 添加到 `~/.codex/hooks.json` 的 `UserPromptSubmit` 和
131
117
  `Stop`, 并把 `at-usage` skill 装入 `~/.agents/skills`. 安装后在 Codex 里运行
132
- `/hooks` 批准一次. Hook 输出只在 Codex CLI 里显示; Paseo 等不显示 hook 输出的
133
- 前端, 可调用 `$at-usage` 按需查询.
118
+ `/hooks` 批准一次. Codex CLI 会显示 hook 输出; 部分客户端(如 Paseo)目前不显示,
119
+ 可调用 `$at-usage` 按需查询.
134
120
  - **OpenCode** — 添加 server plugin 和 TUI plugin: session 进入 idle 后自动刷新
135
121
  usage 并以 toast 显示, `/at-usage` 查看最新缓存值. 安装或更新后需要重启 opencode.
136
122
 
123
+ 中转端点自动发现 — Codex: 读取 `~/.codex/config.toml` / `auth.json` 里当前
124
+ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
125
+ `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_API_KEY`. 官方(非中转)端点会被跳过.
126
+ 如果结果是 `Provider usage is unavailable.`, 可用环境变量
127
+ `PROVIDER_USAGE_BASE_URL` / `PROVIDER_USAGE_API_KEY` 显式指定端点和密钥,
128
+ 并在 `~/.agent-tools/config.jsonc` 里调整 `providerUsage`:
129
+
130
+ ```jsonc
131
+ {
132
+ "providerUsage": {
133
+ "preset": "auto", // sub2api | new-api | veloera | anyrouter | openrouter | ...
134
+ "userId": "", // 部分 NewAPI/Veloera 面板需要填面板用户 id
135
+ "days": 30, // "30d" 字段的统计窗口(最大 90)
136
+ "debug": false // true: 探测过程写入 ~/.agent-tools/logs/usage-debug.log
137
+ }
138
+ }
139
+ ```
140
+
137
141
  显示效果示例:
138
142
 
139
143
  ```text
140
- # 订阅/套餐额度.
144
+ # 中转套餐额度.
141
145
  API | D $0.0/$100 | W $0.0/$300 | Exp 07-08
142
146
 
143
147
  # 钱包余额.
@@ -147,11 +151,6 @@ API | balance $362 | today $61.7 | 30d $566
147
151
  字段含义: `D/W/M` 是日/周/月套餐消耗与上限, `Exp` 是套餐到期日,
148
152
  `balance` 是钱包余额, `today` / `30d` 是今日与近 30 天 API 消耗.
149
153
 
150
- #### 支持的网关
151
-
152
- 余额, 额度和套餐用量查询支持兼容 Sub2API-like,
153
- NewAPI/OneAPI/OneHub/DoneHub/Veloera/AnyRouter-like 与 OpenRouter 的网关.
154
-
155
154
  ### Vision(跨模型识图)
156
155
 
157
156
  让不支持图片的主模型借助多模态模型识图: 针对图片(本地路径或 http(s) URL)提出具体问题, 拿到答案后继续自己的推理. 常见场景: 读取报错截图, 按设计稿还原 UI, 定位测试反馈截图里的界面问题. 一个安装器 capability 包含三部分: `inspect_image` MCP stdio server, `at-vision` 策略 skill, 以及人工诊断 CLI.
@@ -178,8 +177,8 @@ OpenCode: `opencode.json`), 并把 `at-vision` skill 装入对应 agent 的 skil
178
177
  "baseUrl": "https://gateway.example.com/v1", // anthropic-compatible 填网关根地址, 会自动拼 /v1/messages
179
178
  "model": "internal-vlm",
180
179
  "apiKey": { "env": "OPENAI_API_KEY" } // 引用已有环境变量, 也可以直接填密钥
181
- // 可选: "timeoutMs": 30000, "maxImageBytes": 20971520, "maxOutputTokens": 8192,
182
- // "maxConcurrentRequests": 2, "maxRequestsPerMinute": 30
180
+ // , "timeoutMs": 30000, "maxImageBytes": 20971520, "maxOutputTokens": 8192
181
+ // , "maxConcurrentRequests": 2, "maxRequestsPerMinute": 30
183
182
  }
184
183
  }
185
184
  ```
@@ -200,26 +199,41 @@ npx -y @kairyou/agent-tools@latest inspect-image <path|url> -q "导航栏的背
200
199
 
201
200
  ## 从 Git 运行
202
201
 
203
- 如需直接使用仓库版本,可将 npm 包名替换为 `github:kairyou/agent-tools`(本机需已安装 Git):
202
+ 如需直接使用仓库版本, 可将 npm 包名替换为 `github:kairyou/agent-tools` (本机需已安装 Git):
204
203
 
205
204
  ```bash
206
205
  npx -y github:kairyou/agent-tools usage -a codex
207
206
  ```
208
207
 
208
+ ## 仓库结构
209
+
210
+ ```text
211
+ agent-tools/
212
+ ├── .claude-plugin/ # Claude Code/plugin 生态的 manifest.
213
+ ├── .codex-plugin/ # Codex plugin manifest.
214
+ ├── integrations/ # 可安装的 capability, 一个一目录.
215
+ │ ├── statusline/ # Agent 状态栏: 分支, 模型, 用量.
216
+ │ ├── usage/ # Provider 余额/额度显示.
217
+ │ └── vision/ # 跨模型识图.
218
+ ├── skills/ # 可复用的 Agent Skills.
219
+ │ ├── workflow/ # 工作流类 skills.
220
+ │ │ ├── at-commit/ # 生成 Conventional Commits message.
221
+ │ │ ├── at-review/ # 审查改动中的 bug 与回归风险.
222
+ │ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
223
+ │ └── integrations/ # 对接外部系统的 skills.
224
+ │ └── at-zentao/ # 禅道 bug/task 修复工作流.
225
+ └── scripts/ # 安装, 同步, 校验和仓库维护脚本.
226
+ ```
227
+
209
228
  ## FAQ
210
229
 
211
- ### 为什么全局安装提示 PromptScript 不支持?
230
+ ### 为什么全局安装提示 PromptScript 不支持?
212
231
 
213
232
  `PromptScript does not support global skill installation` 表示 PromptScript Agent 不支持全局
214
- 安装,不影响其他 Agent,可忽略。参见 [`skills` issue #1352](https://github.com/vercel-labs/skills/issues/1352)
233
+ 安装, 不影响其他 Agent, 可忽略. 参见 [`skills` issue #1352](https://github.com/vercel-labs/skills/issues/1352).
215
234
 
216
235
  ## References
217
236
 
218
237
  - [OpenCommit](https://github.com/di-sukharev/opencommit)
219
238
  - [GitLens](https://github.com/gitkraken/vscode-gitlens)
220
239
  - [claude-code-system-prompts](https://github.com/Piebald-AI/claude-code-system-prompts)
221
-
222
- ## 说明
223
-
224
- - 安装器只标记并移除自己写入的配置项。
225
- - 本地检查运行 `npm test`。
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+
3
+ // integrations/statusline/claude-statusline.mjs
4
+ import { execFileSync, spawn } from "node:child_process";
5
+ import fs from "node:fs";
6
+ import { basename, dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ var SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
9
+ var AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(SCRIPT_DIR, "..", "..");
10
+ var DEFAULT_CONFIG_FILE = join(AGENT_TOOLS_HOME, "config.jsonc");
11
+ var SNAPSHOT_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
12
+ var REFRESH_STATE_FILE = join(AGENT_TOOLS_HOME, "cache", "usage-refresh-state.json");
13
+ var USAGE_RUNTIME = join(AGENT_TOOLS_HOME, "dist", "usage", "core.mjs");
14
+ var DEFAULT_SNAPSHOT_TTL_MS = 6e4;
15
+ var DEFAULT_REFRESH_COOLDOWN_MS = 3e4;
16
+ var DEFAULT_FAILURE_BACKOFF_MS = 12e4;
17
+ var DEFAULT_CONFIG = {
18
+ fields: ["branch", "model", "fiveHour", "week"],
19
+ separator: " | ",
20
+ symbols: {
21
+ branch: "\u2387",
22
+ reset: "\u27F3",
23
+ empty: "\u2013",
24
+ fiveHour: "5h",
25
+ week: "w",
26
+ context: "ctx"
27
+ }
28
+ };
29
+ var FIELD_ALIASES = {
30
+ cwd: "directory",
31
+ dir: "directory",
32
+ five: "fiveHour",
33
+ five_hour: "fiveHour",
34
+ "5h": "fiveHour",
35
+ sevenDay: "week",
36
+ seven_day: "week",
37
+ "7d": "week",
38
+ weekly: "week",
39
+ ctx: "context"
40
+ };
41
+ async function readStdin() {
42
+ const chunks = [];
43
+ for await (const chunk of process.stdin) chunks.push(chunk);
44
+ return Buffer.concat(chunks).toString("utf8");
45
+ }
46
+ function readJsonFile(file) {
47
+ try {
48
+ if (!fs.existsSync(file)) return {};
49
+ const raw = stripTrailingCommas(stripJsonComments(fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "")));
50
+ return raw.trim() ? JSON.parse(raw) : {};
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+ function stripJsonComments(input) {
56
+ let out = "";
57
+ let inString = false;
58
+ let escaped = false;
59
+ for (let i = 0; i < input.length; i++) {
60
+ const ch = input[i];
61
+ const next = input[i + 1];
62
+ if (inString) {
63
+ out += ch;
64
+ escaped = ch === "\\" ? !escaped : false;
65
+ if (ch === '"' && !escaped) inString = false;
66
+ continue;
67
+ }
68
+ if (ch === '"') {
69
+ inString = true;
70
+ out += ch;
71
+ continue;
72
+ }
73
+ if (ch === "/" && next === "/") {
74
+ while (i < input.length && input[i] !== "\n") i++;
75
+ out += "\n";
76
+ continue;
77
+ }
78
+ if (ch === "/" && next === "*") {
79
+ i += 2;
80
+ while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
81
+ i++;
82
+ continue;
83
+ }
84
+ out += ch;
85
+ }
86
+ return out;
87
+ }
88
+ function stripTrailingCommas(input) {
89
+ let out = "";
90
+ let inString = false;
91
+ let escaped = false;
92
+ for (let i = 0; i < input.length; i++) {
93
+ const ch = input[i];
94
+ if (inString) {
95
+ out += ch;
96
+ escaped = ch === "\\" ? !escaped : false;
97
+ if (ch === '"' && !escaped) inString = false;
98
+ continue;
99
+ }
100
+ if (ch === '"') {
101
+ inString = true;
102
+ out += ch;
103
+ continue;
104
+ }
105
+ if (ch === ",") {
106
+ let j = i + 1;
107
+ while (j < input.length && /\s/.test(input[j])) j++;
108
+ if (input[j] === "}" || input[j] === "]") continue;
109
+ }
110
+ out += ch;
111
+ }
112
+ return out;
113
+ }
114
+ function parseArgs(argv) {
115
+ const opts = {};
116
+ for (let i = 0; i < argv.length; i++) {
117
+ const arg = argv[i];
118
+ if (arg === "--fields" && argv[i + 1]) {
119
+ opts.fields = argv[++i];
120
+ } else if (arg.startsWith("--fields=")) {
121
+ opts.fields = arg.slice("--fields=".length);
122
+ } else if (arg === "--separator" && argv[i + 1]) {
123
+ opts.separator = argv[++i];
124
+ } else if (arg.startsWith("--separator=")) {
125
+ opts.separator = arg.slice("--separator=".length);
126
+ }
127
+ }
128
+ return opts;
129
+ }
130
+ function splitFields(value) {
131
+ if (Array.isArray(value)) return value;
132
+ if (typeof value !== "string") return null;
133
+ return value.split(/[,\s]+/).map((part) => part.trim()).filter(Boolean);
134
+ }
135
+ function normalizeField(field) {
136
+ return FIELD_ALIASES[field] || field;
137
+ }
138
+ function mergeConfig(cli) {
139
+ const rootConfig = readJsonFile(DEFAULT_CONFIG_FILE);
140
+ const fileConfig = rootConfig.statusline || {};
141
+ const envFields = process.env.AGENT_TOOLS_STATUSLINE_FIELDS;
142
+ const envSeparator = process.env.AGENT_TOOLS_STATUSLINE_SEPARATOR;
143
+ const config = {
144
+ ...DEFAULT_CONFIG,
145
+ ...fileConfig,
146
+ symbols: { ...DEFAULT_CONFIG.symbols, ...fileConfig.symbols || {} }
147
+ };
148
+ const fields = splitFields(cli.fields) || splitFields(envFields) || splitFields(fileConfig.fields) || DEFAULT_CONFIG.fields;
149
+ config.fields = fields.map(normalizeField);
150
+ if (typeof envSeparator === "string") config.separator = envSeparator;
151
+ if (typeof cli.separator === "string") config.separator = cli.separator;
152
+ return config;
153
+ }
154
+ function gitBranch(cwd) {
155
+ try {
156
+ const out = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
157
+ cwd,
158
+ stdio: ["ignore", "pipe", "ignore"],
159
+ encoding: "utf8"
160
+ }).trim();
161
+ return out && out !== "HEAD" ? out : "";
162
+ } catch {
163
+ return "";
164
+ }
165
+ }
166
+ function secondsUntil(unixSeconds) {
167
+ if (!unixSeconds) return null;
168
+ const seconds = Number(unixSeconds) - Math.floor(Date.now() / 1e3);
169
+ return Number.isFinite(seconds) ? Math.max(0, seconds) : null;
170
+ }
171
+ function compactDuration(totalSeconds) {
172
+ if (totalSeconds == null) return "";
173
+ if (totalSeconds <= 0) return "0m";
174
+ let seconds = totalSeconds;
175
+ const days = Math.floor(seconds / 86400);
176
+ seconds %= 86400;
177
+ const hours = Math.floor(seconds / 3600);
178
+ seconds %= 3600;
179
+ const minutes = Math.floor(seconds / 60);
180
+ if (days) return `${days}d${hours}h`;
181
+ if (hours) return `${hours}h${minutes}m`;
182
+ return `${minutes}m`;
183
+ }
184
+ function usageWindow(window, config) {
185
+ if (!window || typeof window.used_percentage !== "number") {
186
+ return "";
187
+ }
188
+ const pct = `${Math.round(window.used_percentage)}%`;
189
+ const left = compactDuration(secondsUntil(window.resets_at));
190
+ return left ? `${pct} ${config.symbols.reset}${left}` : pct;
191
+ }
192
+ function showMissingUsageWindow() {
193
+ const baseUrl = activeRelayBaseUrl();
194
+ return !baseUrl || isOfficialBaseUrl(baseUrl);
195
+ }
196
+ function shortModelName(name) {
197
+ if (!name) return "";
198
+ return String(name).replace(/^Claude\s+/i, "").replace(/\s*\[1m\]\s*$/i, "").trim();
199
+ }
200
+ function renderField(field, data, config) {
201
+ const dir = data?.workspace?.current_dir || data?.cwd || process.cwd() || "";
202
+ const projectDir = data?.workspace?.project_dir || dir;
203
+ switch (field) {
204
+ case "branch": {
205
+ const branch = gitBranch(projectDir);
206
+ return branch ? `${config.symbols.branch} ${branch}` : "";
207
+ }
208
+ case "model":
209
+ return shortModelName(data?.model?.display_name || data?.model?.id || "");
210
+ case "fiveHour": {
211
+ const value = usageWindow(data?.rate_limits?.five_hour, config);
212
+ return value || showMissingUsageWindow() ? `${config.symbols.fiveHour} ${value || config.symbols.empty}` : "";
213
+ }
214
+ case "week": {
215
+ const value = usageWindow(data?.rate_limits?.seven_day, config);
216
+ return value || showMissingUsageWindow() ? `${config.symbols.week} ${value || config.symbols.empty}` : "";
217
+ }
218
+ case "context": {
219
+ const pct = data?.context_window?.used_percentage;
220
+ return typeof pct === "number" ? `${config.symbols.context} ${Math.round(pct)}%` : "";
221
+ }
222
+ case "directory":
223
+ return dir ? basename(dir) : "";
224
+ default:
225
+ return "";
226
+ }
227
+ }
228
+ function numberFromEnv(name, fallback) {
229
+ const value = Number(process.env[name]);
230
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
231
+ }
232
+ function cleanBaseUrl(baseUrl) {
233
+ return String(baseUrl || "").replace(/\/+$/, "");
234
+ }
235
+ function isOfficialBaseUrl(baseUrl) {
236
+ if (!baseUrl) return true;
237
+ const clean = cleanBaseUrl(baseUrl);
238
+ return [
239
+ "https://api.anthropic.com",
240
+ "https://api.anthropic.com/v1",
241
+ "https://api.openai.com",
242
+ "https://api.openai.com/v1"
243
+ ].includes(clean);
244
+ }
245
+ function usageRouteCacheKey(baseUrl) {
246
+ try {
247
+ const url = new URL(cleanBaseUrl(baseUrl));
248
+ url.hash = "";
249
+ url.search = "";
250
+ url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/i, "").replace(/\/v1$/i, "");
251
+ return url.toString().replace(/\/$/, "");
252
+ } catch {
253
+ return cleanBaseUrl(baseUrl).endsWith("/v1") ? cleanBaseUrl(baseUrl).slice(0, -3) : cleanBaseUrl(baseUrl);
254
+ }
255
+ }
256
+ function readJsonFileRaw(file) {
257
+ try {
258
+ if (!fs.existsSync(file)) return {};
259
+ const raw = fs.readFileSync(file, "utf8").replace(/^\uFEFF/, "");
260
+ return raw.trim() ? JSON.parse(raw) : {};
261
+ } catch {
262
+ return {};
263
+ }
264
+ }
265
+ function activeRelayBaseUrl() {
266
+ return process.env.PROVIDER_USAGE_BASE_URL || process.env.ANTHROPIC_BASE_URL || "";
267
+ }
268
+ function hasClaudeUsageToken() {
269
+ return Boolean(
270
+ process.env.PROVIDER_USAGE_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY
271
+ );
272
+ }
273
+ function snapshotForBaseUrl(baseUrl) {
274
+ const snapshot = readJsonFileRaw(SNAPSHOT_FILE);
275
+ const key = usageRouteCacheKey(baseUrl);
276
+ const item = snapshot?.items?.[key];
277
+ return item?.text ? item : null;
278
+ }
279
+ function refreshStateForBaseUrl(baseUrl) {
280
+ const state = readJsonFileRaw(REFRESH_STATE_FILE);
281
+ return state?.items?.[usageRouteCacheKey(baseUrl)] || {};
282
+ }
283
+ function ageMs(isoDate) {
284
+ const time = Date.parse(isoDate || "");
285
+ return Number.isFinite(time) ? Date.now() - time : Number.POSITIVE_INFINITY;
286
+ }
287
+ function writeRefreshAttempt(baseUrl) {
288
+ try {
289
+ const state = readJsonFileRaw(REFRESH_STATE_FILE);
290
+ const key = usageRouteCacheKey(baseUrl);
291
+ state.version = 1;
292
+ state.items = state.items && typeof state.items === "object" ? state.items : {};
293
+ state.items[key] = {
294
+ ...state.items[key] || {},
295
+ baseUrl,
296
+ lastAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
297
+ };
298
+ fs.mkdirSync(dirname(REFRESH_STATE_FILE), { recursive: true });
299
+ fs.writeFileSync(REFRESH_STATE_FILE, `${JSON.stringify(state, null, 2)}
300
+ `);
301
+ } catch {
302
+ }
303
+ }
304
+ function shouldRefreshUsage(baseUrl, snapshot) {
305
+ if (process.env.AGENT_TOOLS_USAGE_REFRESH === "0") return false;
306
+ if (!baseUrl || isOfficialBaseUrl(baseUrl)) return false;
307
+ if (!hasClaudeUsageToken()) return false;
308
+ if (!fs.existsSync(USAGE_RUNTIME)) return false;
309
+ const ttlMs = numberFromEnv("AGENT_TOOLS_USAGE_SNAPSHOT_TTL_MS", DEFAULT_SNAPSHOT_TTL_MS);
310
+ const cooldownMs = numberFromEnv("AGENT_TOOLS_USAGE_REFRESH_COOLDOWN_MS", DEFAULT_REFRESH_COOLDOWN_MS);
311
+ const failureBackoffMs = numberFromEnv("AGENT_TOOLS_USAGE_FAILURE_BACKOFF_MS", DEFAULT_FAILURE_BACKOFF_MS);
312
+ const state = refreshStateForBaseUrl(baseUrl);
313
+ if (ageMs(state.lastAttemptAt) < cooldownMs) return false;
314
+ if (state.lastError && ageMs(state.lastFailureAt) < failureBackoffMs) return false;
315
+ return !snapshot || ageMs(snapshot.updatedAt) >= ttlMs;
316
+ }
317
+ function refreshUsageInBackground(baseUrl) {
318
+ if (!shouldRefreshUsage(baseUrl, snapshotForBaseUrl(baseUrl))) return;
319
+ writeRefreshAttempt(baseUrl);
320
+ try {
321
+ const child = spawn(process.execPath, [USAGE_RUNTIME, "refresh", "--agent", "claude"], {
322
+ detached: true,
323
+ stdio: "ignore",
324
+ env: process.env,
325
+ windowsHide: true
326
+ });
327
+ child.unref();
328
+ } catch {
329
+ }
330
+ }
331
+ function providerUsageStatus() {
332
+ const baseUrl = activeRelayBaseUrl();
333
+ if (!baseUrl || isOfficialBaseUrl(baseUrl)) return "";
334
+ const snapshot = snapshotForBaseUrl(baseUrl);
335
+ if (shouldRefreshUsage(baseUrl, snapshot)) refreshUsageInBackground(baseUrl);
336
+ return snapshot?.text || "";
337
+ }
338
+ function render(data, config) {
339
+ const fields = config.fields.map((field) => renderField(field, data, config)).filter(Boolean);
340
+ const providerUsage = providerUsageStatus();
341
+ if (providerUsage) fields.push(providerUsage);
342
+ return fields.join(config.separator);
343
+ }
344
+ async function main() {
345
+ const config = mergeConfig(parseArgs(process.argv.slice(2)));
346
+ let data = {};
347
+ try {
348
+ const raw = await readStdin();
349
+ data = raw.trim() ? JSON.parse(raw) : {};
350
+ } catch {
351
+ data = {};
352
+ }
353
+ process.stdout.write(render(data, config));
354
+ }
355
+ main();