@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 +56 -39
- package/README.zh-CN.md +54 -40
- package/dist/statusline/claude-statusline.mjs +355 -0
- package/dist/usage/cli.mjs +25 -0
- package/dist/usage/codex-hook.mjs +144 -0
- package/dist/usage/core.mjs +1054 -0
- package/dist/usage/opencode-plugin.mjs +80 -0
- package/dist/usage/opencode-tui.mjs +46 -0
- package/integrations/statusline/claude-statusline.mjs +32 -3
- package/integrations/usage/core.mjs +30 -1
- package/package.json +5 -5
- package/scripts/build.mjs +65 -0
- package/scripts/install.mjs +26 -15
- package/scripts/build-vision.mjs +0 -35
package/README.md
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
# Agent Tools
|
|
2
2
|
|
|
3
|
-
Reusable Agent Skills
|
|
3
|
+
Reusable Agent Skills and installable integrations — statusline, provider usage, and vision — for Codex, Claude Code, and opencode.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Repository Structure
|
|
5
|
+
Requires Node.js >= 22.
|
|
8
6
|
|
|
9
|
-
|
|
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`
|
|
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
|
-
|
|
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.
|
|
138
|
-
|
|
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
|
-
#
|
|
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
|
-
//
|
|
189
|
-
//
|
|
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, 以及面向 Codex
|
|
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
|
-
|
|
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` 批准一次.
|
|
133
|
-
|
|
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
|
-
//
|
|
182
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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();
|