agent-runway 0.2.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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +27 -0
- package/LICENSE +21 -0
- package/README.md +376 -0
- package/package.json +52 -0
- package/skills/agent-runway/SKILL.md +92 -0
- package/src/cache.mjs +90 -0
- package/src/cli.mjs +200 -0
- package/src/core.mjs +300 -0
- package/src/installs.mjs +425 -0
- package/src/mcp.mjs +279 -0
- package/src/models.mjs +303 -0
- package/src/providers/antigravity.mjs +132 -0
- package/src/providers/claude.mjs +51 -0
- package/src/providers/codex.mjs +119 -0
- package/src/providers/copilot.mjs +87 -0
- package/src/providers/index.mjs +155 -0
- package/src/providers/shared.mjs +123 -0
- package/src/render.mjs +162 -0
- package/src/setup.mjs +412 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-runway",
|
|
3
|
+
"description": "Single-plugin marketplace for agent-runway.",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "jberdah",
|
|
6
|
+
"url": "https://github.com/jberdah"
|
|
7
|
+
},
|
|
8
|
+
"plugins": [
|
|
9
|
+
{
|
|
10
|
+
"name": "agent-runway",
|
|
11
|
+
"displayName": "Agent Runway",
|
|
12
|
+
"source": "./",
|
|
13
|
+
"description": "Quota and model discovery for the coding agents installed on this machine: how much runway each has left, and which model slugs each binary will actually accept.",
|
|
14
|
+
"version": "0.2.0",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "jberdah",
|
|
17
|
+
"url": "https://github.com/jberdah"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/jberdah/agent-runway#readme",
|
|
20
|
+
"repository": "https://github.com/jberdah/agent-runway",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"category": "productivity",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"usage",
|
|
25
|
+
"quota",
|
|
26
|
+
"rate-limit",
|
|
27
|
+
"mcp"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-runway",
|
|
3
|
+
"displayName": "Agent Runway",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "Quota and model discovery for the coding agents installed on this machine: how much runway each has left, and which model slugs each binary will actually accept.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "jberdah",
|
|
8
|
+
"url": "https://github.com/jberdah"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/jberdah/agent-runway#readme",
|
|
11
|
+
"repository": "https://github.com/jberdah/agent-runway",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"usage",
|
|
15
|
+
"quota",
|
|
16
|
+
"rate-limit",
|
|
17
|
+
"mcp"
|
|
18
|
+
],
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"agent-runway": {
|
|
21
|
+
"command": "node",
|
|
22
|
+
"args": [
|
|
23
|
+
"${CLAUDE_PLUGIN_ROOT}/src/mcp.mjs"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jberdah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# agent-runway
|
|
2
|
+
|
|
3
|
+
How much runway is left before your coding agent hits a rate limit — which
|
|
4
|
+
windows are consumed, and when each one resets.
|
|
5
|
+
|
|
6
|
+
Same answer three ways: a **CLI**, a **Claude Code skill**, and an **MCP tool**
|
|
7
|
+
any client can call, so the reasoning happens once instead of in every model.
|
|
8
|
+
|
|
9
|
+
> **Unofficial.** This reads undocumented Anthropic endpoints. It can stop
|
|
10
|
+
> working without notice and is not a compatibility contract with anyone.
|
|
11
|
+
|
|
12
|
+
Covers **Claude, Codex, GitHub Copilot, Gemini CLI and Antigravity**, each read
|
|
13
|
+
with the credentials that agent already keeps. One provider failing never costs
|
|
14
|
+
the others their answer.
|
|
15
|
+
|
|
16
|
+
*Part of the [brainclaw](https://brainclaw.dev) toolkit.*
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
$ agent-runway
|
|
20
|
+
|
|
21
|
+
Runway - Claude
|
|
22
|
+
|
|
23
|
+
Session (5h) [###.................] 15 %
|
|
24
|
+
resets 2026-09-12 02:40Z (in 4h 44m)
|
|
25
|
+
* Weekly - all models [################....] 79 % <-- warning
|
|
26
|
+
resets 2026-09-15 03:00Z (in 3d 5h)
|
|
27
|
+
Weekly - Opus [##########..........] 52 %
|
|
28
|
+
resets 2026-09-15 03:00Z (in 3d 5h)
|
|
29
|
+
|
|
30
|
+
Extra usage credits: disabled
|
|
31
|
+
|
|
32
|
+
* = closest to its limit, as the API flags it
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
Claude Code shows this behind `/usage`, interactively. That does not help an
|
|
38
|
+
agent decide whether a long task or a fan-out of subagents will fit in the
|
|
39
|
+
remaining budget, and it does not help a script. This exposes the same numbers
|
|
40
|
+
to the terminal, to a skill, and to any MCP client.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
### Quickest path
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g agent-runway
|
|
48
|
+
agent-runway setup
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or without installing anything globally:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/jberdah/agent-runway && cd agent-runway
|
|
55
|
+
node src/cli.mjs setup
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`setup` is one guided pass, identical on Windows, macOS and Linux. It reads a
|
|
59
|
+
token back without echoing it, **validates it against the API before saving
|
|
60
|
+
anything**, then writes it to `~/.claude/usage-token` with mode 0600 and prints
|
|
61
|
+
your current usage. `setup --stdin` takes the token from a pipe instead, so it
|
|
62
|
+
never has to be displayed or pasted.
|
|
63
|
+
|
|
64
|
+
Most of the tool needs no credential at all: `--models` and `resolve` read local
|
|
65
|
+
binaries. Only quota requires signing in.
|
|
66
|
+
|
|
67
|
+
Re-running it is safe: it reports that things already work and changes nothing
|
|
68
|
+
unless you pass `--force`.
|
|
69
|
+
|
|
70
|
+
### As a Claude Code plugin (skill + MCP tool)
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
/plugin marketplace add jberdah/agent-runway
|
|
74
|
+
/plugin install agent-runway@agent-runway
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Claude then reads your usage whenever it is relevant — ask "how much quota do I
|
|
78
|
+
have left?" or let it check before a long task.
|
|
79
|
+
|
|
80
|
+
### As an MCP server, in any client
|
|
81
|
+
|
|
82
|
+
No install step: it has no dependencies, so pointing a client at the file is
|
|
83
|
+
enough.
|
|
84
|
+
|
|
85
|
+
```jsonc
|
|
86
|
+
// Claude Desktop: claude_desktop_config.json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"agent-runway": {
|
|
90
|
+
"command": "node",
|
|
91
|
+
"args": ["/absolute/path/to/agent-runway/src/mcp.mjs"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
For Claude Code specifically:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
claude mcp add agent-runway -- node /absolute/path/to/agent-runway/src/mcp.mjs
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### As a CLI
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install -g agent-runway # or: git clone && npm link
|
|
107
|
+
agent-runway --short
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Cloning is enough to run it — `node src/cli.mjs` needs nothing installed.
|
|
111
|
+
|
|
112
|
+
## Authentication
|
|
113
|
+
|
|
114
|
+
`agent-runway setup` handles this. What follows is what it does, for anyone who
|
|
115
|
+
would rather do it by hand or automate it.
|
|
116
|
+
|
|
117
|
+
### Claude has no durable credential today
|
|
118
|
+
|
|
119
|
+
This is the project's sharpest limitation, and it was found by trying rather
|
|
120
|
+
than by reading docs.
|
|
121
|
+
|
|
122
|
+
`claude setup-token` mints a long-lived token, and it does **not** work here:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
HTTP 403 - OAuth token does not meet scope requirement user:profile
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
That command grants inference scopes. The usage endpoint wants `user:profile`,
|
|
129
|
+
which it does not issue, so regenerating the token produces the same refusal
|
|
130
|
+
every time. There is no flag to ask for a wider scope.
|
|
131
|
+
|
|
132
|
+
What does carry `user:profile` is the session credential Claude Code keeps for
|
|
133
|
+
itself, and that is refreshed only while Claude Code is running. So:
|
|
134
|
+
|
|
135
|
+
| Situation | Claude usage readable |
|
|
136
|
+
| --- | --- |
|
|
137
|
+
| Claude Code in active use | yes |
|
|
138
|
+
| Claude Code idle for hours, or signed out | no |
|
|
139
|
+
| A scheduled job on an otherwise quiet machine | no |
|
|
140
|
+
|
|
141
|
+
In practice this bites less than it sounds: an agent checking its own runway
|
|
142
|
+
mid-task is running inside Claude Code, so the credential is fresh exactly when
|
|
143
|
+
it is needed. What it does rule out is the unattended case — waking up at a
|
|
144
|
+
reset to see whether the quota came back.
|
|
145
|
+
|
|
146
|
+
**Codex, Copilot and Antigravity are unaffected.** Each has a durable credential
|
|
147
|
+
of its own, which is part of why this tool covers more than one provider.
|
|
148
|
+
|
|
149
|
+
Refreshing Claude Code's token ourselves is possible in principle and is
|
|
150
|
+
deliberately not done: the refresh token rotates on use, so a background tool
|
|
151
|
+
racing Claude Code for it could sign the user out of their own editor.
|
|
152
|
+
|
|
153
|
+
### The one durable path: a session cookie
|
|
154
|
+
|
|
155
|
+
There are two usage endpoints, and they take different credentials:
|
|
156
|
+
|
|
157
|
+
| Endpoint | Credential | Verified |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `api.anthropic.com/api/oauth/usage` | Bearer OAuth, needs `user:profile` | 200 with Claude Code's session token, 403 with `setup-token` |
|
|
160
|
+
| `claude.ai/api/organizations/{org}/usage` | **cookie only** | 403 to any Bearer: *"This endpoint does not accept OAuth access tokens"* |
|
|
161
|
+
|
|
162
|
+
So the claude.ai endpoint is not a fallback for the first — it is a different
|
|
163
|
+
door. Give it the `sessionKey` cookie from a signed-in claude.ai browser
|
|
164
|
+
session and it answers, no OAuth scope involved, and it keeps working while
|
|
165
|
+
Claude Code is closed:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# the value of the sessionKey cookie, pasted by you
|
|
169
|
+
echo "sk-ant-sid01-..." > "$HOME/.claude/session-cookie" # or AGENT_RUNWAY_CLAUDE_COOKIE
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Weigh it honestly before using it. A session cookie is a **broader credential
|
|
173
|
+
than an OAuth token** — it is the browser's full account session, not a scoped
|
|
174
|
+
grant. It dies when you sign out, and it cannot be narrowed. This tool will
|
|
175
|
+
never read it out of a browser profile: you paste it, or you go without.
|
|
176
|
+
|
|
177
|
+
### Why a file rather than an environment variable
|
|
178
|
+
|
|
179
|
+
Setup writes the token to `~/.claude/usage-token` (mode 0600) instead of
|
|
180
|
+
exporting it. On macOS and Linux, a persistent environment variable means
|
|
181
|
+
writing the secret into a shell rc file, which is commonly mode 644 and
|
|
182
|
+
sometimes committed to a dotfiles repository. One 0600 file is safer, behaves
|
|
183
|
+
identically on all three platforms, is picked up by every invocation whatever
|
|
184
|
+
your shell, and is revoked by deleting it.
|
|
185
|
+
|
|
186
|
+
`agent-runway setup --env` still wires up the variable if you want it. On POSIX
|
|
187
|
+
it appends an indirection rather than a second copy of the secret:
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
export AGENT_RUNWAY_TOKEN="$(cat $HOME/.claude/usage-token 2>/dev/null)"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
On Windows it sets a user-level variable, passing the value through stdin so the
|
|
194
|
+
token never appears in a process list.
|
|
195
|
+
|
|
196
|
+
Environment variables remain the right mechanism in CI, where the secret comes
|
|
197
|
+
from the platform's own secret store.
|
|
198
|
+
|
|
199
|
+
Sources are tried in this order, first match wins:
|
|
200
|
+
|
|
201
|
+
| Source | Notes |
|
|
202
|
+
| --- | --- |
|
|
203
|
+
| `CLAUDE_CODE_OAUTH_TOKEN` | a token carrying `user:profile` — note that `claude setup-token` does not mint one |
|
|
204
|
+
| `AGENT_RUNWAY_TOKEN` | if you want a variable scoped to this tool |
|
|
205
|
+
| `ANTHROPIC_AUTH_TOKEN` | already set in many setups |
|
|
206
|
+
| `~/.claude/usage-token` | a file holding the token on one line |
|
|
207
|
+
| `~/.claude/.credentials.json` | Claude Code's own session token |
|
|
208
|
+
|
|
209
|
+
The last one makes the tool work with no setup at all, but it is a convenience
|
|
210
|
+
rather than a contract: on macOS the live token lives in the Keychain and on
|
|
211
|
+
Windows in the Credential Manager, so that file is often absent or stale. Set
|
|
212
|
+
`AGENT_RUNWAY_NO_LOCAL_CREDENTIALS=1` to skip it entirely.
|
|
213
|
+
|
|
214
|
+
`CLAUDE_ORG_ID` overrides the organization UUID, which is only needed by the
|
|
215
|
+
claude.ai fallback endpoint.
|
|
216
|
+
|
|
217
|
+
## Two questions, one tool
|
|
218
|
+
|
|
219
|
+
Before delegating work, an agent needs both halves of the answer, and getting
|
|
220
|
+
them from two different tools defeats the point:
|
|
221
|
+
|
|
222
|
+
**How much runway is left, per provider.**
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
agent-runway --all
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
Claude Session (5h) 4 % | Weekly - all models 87 %
|
|
230
|
+
OpenAI Codex plus Session 0 % | Weekly 0 %
|
|
231
|
+
GitHub Copilot Chat 200/200 requests | Premium: not included in this plan
|
|
232
|
+
Antigravity Pro Flow credits 100 %
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Which models each install will actually accept.**
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
agent-runway --models
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
codex path 0.149.1 4 models [declared]
|
|
243
|
+
gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5
|
|
244
|
+
codex vscode 0.154.0-alpha.6.1 5 models [declared]
|
|
245
|
+
gpt-6-astra, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, gpt-5.5
|
|
246
|
+
|
|
247
|
+
Disagreements between installs of the same agent:
|
|
248
|
+
codex path 0.149.1 does not offer: gpt-6-astra
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
That last line is the point. A machine carries several builds of the same agent
|
|
252
|
+
— five of Claude here — and they do not agree. `gpt-6-astra` is what this
|
|
253
|
+
machine's `config.toml` selects: it runs under the editor extension and does not
|
|
254
|
+
exist for the CLI a delegation would spawn.
|
|
255
|
+
|
|
256
|
+
Each catalogue says how it was obtained. **declared** means the binary was asked
|
|
257
|
+
and answered, through `codex app-server`'s `model/list` or the Copilot CLI's own
|
|
258
|
+
shell completion. **inferred** means identifiers were read out of the binary,
|
|
259
|
+
which Claude Code requires because it exposes no list: strong evidence, not a
|
|
260
|
+
contract, and occasionally plausible-looking rubbish.
|
|
261
|
+
|
|
262
|
+
## Before spawning another agent
|
|
263
|
+
|
|
264
|
+
One call returns everything needed to build a command that works:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
agent-runway resolve codex --model gpt-6-astra
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```jsonc
|
|
271
|
+
{
|
|
272
|
+
"contract": 1,
|
|
273
|
+
"binary": "…/OpenAI/Codex/bin/codex.exe",
|
|
274
|
+
"version": "0.149.1",
|
|
275
|
+
"models": ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5"],
|
|
276
|
+
"model": {
|
|
277
|
+
"requested": "gpt-6-astra",
|
|
278
|
+
"valid": false,
|
|
279
|
+
"availableIn": [{ "kind": "vscode", "version": "0.154.0-alpha.6.1", "path": "…" }],
|
|
280
|
+
"suggestion": "gpt-5.6-sol"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Exit 1 when the agent cannot be resolved or the slug is refused, so a script can
|
|
286
|
+
branch. The substitute is **offered, never applied**: running a different model
|
|
287
|
+
than was asked for, silently, is worse than failing.
|
|
288
|
+
|
|
289
|
+
This is meant to compose with tools that already know how to build command
|
|
290
|
+
lines. It deliberately does not spawn anything.
|
|
291
|
+
|
|
292
|
+
## Deciding, rather than reporting
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
agent-runway --gate 90
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Prints JSON and answers in the exit code: **0** proceed, **10** defer, **11**
|
|
299
|
+
unknown. Three outcomes rather than two, because a provider that could not be
|
|
300
|
+
read has not got room — it is simply unknown, and must never be counted as
|
|
301
|
+
either.
|
|
302
|
+
|
|
303
|
+
The recommendation states its own rule and whether the candidates were even
|
|
304
|
+
comparable: 0% of a five-hour window is not 0% of a monthly allowance.
|
|
305
|
+
|
|
306
|
+
## CLI reference
|
|
307
|
+
|
|
308
|
+
| Flag | Output |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| *(none)* | Claude only, readable table |
|
|
311
|
+
| `--all` | Every provider found on this machine |
|
|
312
|
+
| `--models` | What each install accepts, and where installs disagree |
|
|
313
|
+
| `resolve <agent>` | Binary, valid slugs and a verdict on one model |
|
|
314
|
+
| `--gate <N>` | Decision as JSON plus an exit code |
|
|
315
|
+
| `--short` | One line: `session=15% weekly_all=79% weekly_scoped=52%` |
|
|
316
|
+
| `--json` | Raw response; combines with `--models` |
|
|
317
|
+
| `--plain` | Table without the header |
|
|
318
|
+
|
|
319
|
+
| Exit code | Meaning |
|
|
320
|
+
| --- | --- |
|
|
321
|
+
| 0 | success |
|
|
322
|
+
| 1 | unexpected error |
|
|
323
|
+
| 2 | no token found |
|
|
324
|
+
| 3 | token rejected — regenerate it |
|
|
325
|
+
| 4 | the usage endpoint is throttling; not your quota |
|
|
326
|
+
|
|
327
|
+
## Security
|
|
328
|
+
|
|
329
|
+
- **No token is ever printed, logged or returned**, not even a prefix. A unit
|
|
330
|
+
test asserts this against every rendered output.
|
|
331
|
+
- The MCP tool returns usage numbers only, never credentials.
|
|
332
|
+
- The tool never runs `claude setup-token` for you, and never reads browser
|
|
333
|
+
cookies or an OS keychain.
|
|
334
|
+
- Read-only: every request is a `GET`.
|
|
335
|
+
|
|
336
|
+
## How it works
|
|
337
|
+
|
|
338
|
+
Two internal endpoints, tried in order, both authenticated with
|
|
339
|
+
`Authorization: Bearer sk-ant-oat01-...` and `anthropic-beta: oauth-2025-04-20`:
|
|
340
|
+
|
|
341
|
+
1. `https://api.anthropic.com/api/oauth/usage`
|
|
342
|
+
2. `https://claude.ai/api/organizations/{org}/usage`
|
|
343
|
+
|
|
344
|
+
**These endpoints are internal and undocumented. They can change or disappear
|
|
345
|
+
without notice.** Parsing is written to degrade rather than break: it prefers
|
|
346
|
+
the canonical `limits` array, falls back to scanning top-level window objects,
|
|
347
|
+
and prints raw JSON if it recognises nothing. If the output ever looks wrong,
|
|
348
|
+
`--json` shows exactly what the API returned.
|
|
349
|
+
|
|
350
|
+
## Development
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
npm install # dev only: the MCP SDK, used to test the server as a real client
|
|
354
|
+
npm test # unit tests, no network
|
|
355
|
+
npm run smoke # drives the MCP server over stdio; needs network and a token
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The runtime has **zero dependencies**. `src/mcp.mjs` speaks JSON-RPC directly
|
|
359
|
+
rather than importing the MCP SDK, because a Claude Code plugin installed from
|
|
360
|
+
git is never `npm install`ed — an imported dependency would have to be vendored.
|
|
361
|
+
`scripts/smoke-mcp.mjs` connects the official SDK client to it, so the
|
|
362
|
+
hand-rolled framing is checked against the real implementation.
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
src/core.mjs token resolution, HTTP, response normalization
|
|
366
|
+
src/render.mjs text rendering
|
|
367
|
+
src/cli.mjs CLI entry point, also what the skill shells out to
|
|
368
|
+
src/setup.mjs guided cross-platform first-time setup
|
|
369
|
+
src/mcp.mjs MCP stdio server
|
|
370
|
+
skills/ the Claude Code skill
|
|
371
|
+
.claude-plugin/ plugin and marketplace manifests
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## License
|
|
375
|
+
|
|
376
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-runway",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Quota and model discovery for the coding agents installed on this machine: how much runway each has left, and which model slugs each binary will actually accept.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "jberdah (https://github.com/jberdah)",
|
|
8
|
+
"homepage": "https://github.com/jberdah/agent-runway#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/jberdah/agent-runway.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/jberdah/agent-runway/issues"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"agent-runway": "src/cli.mjs",
|
|
21
|
+
"agent-runway-mcp": "src/mcp.mjs"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./src/core.mjs"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"skills",
|
|
29
|
+
".claude-plugin",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
35
|
+
"start": "node src/cli.mjs",
|
|
36
|
+
"mcp": "node src/mcp.mjs",
|
|
37
|
+
"smoke": "node scripts/smoke-mcp.mjs",
|
|
38
|
+
"prepublishOnly": "npm test",
|
|
39
|
+
"pack:check": "npm pack --dry-run"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"claude",
|
|
43
|
+
"claude-code",
|
|
44
|
+
"mcp",
|
|
45
|
+
"usage",
|
|
46
|
+
"rate-limit",
|
|
47
|
+
"quota"
|
|
48
|
+
],
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@modelcontextprotocol/sdk": "^1.30.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-runway
|
|
3
|
+
description: Answers two questions about the coding agents installed on this machine - how much rate-limit quota each has left, and which model slugs each installed binary will actually accept. Use when the user asks how much usage or quota is left, whether they are near a limit, when a window resets, or which models are available; and before delegating work to another agent, to pick a binary and a model argument that will not be rejected. Covers Claude, Codex, GitHub Copilot, Gemini CLI and Antigravity.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Agent runway
|
|
7
|
+
|
|
8
|
+
Two questions, one tool: **is there room to work**, and **what can actually be
|
|
9
|
+
run**.
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
All of these are safe, read-only, and print to stdout.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
agent-runway --all # quota for every provider found
|
|
17
|
+
agent-runway --models # what each install accepts, and where installs disagree
|
|
18
|
+
agent-runway --gate 90 # decision + exit code: 0 proceed, 10 defer, 11 unknown
|
|
19
|
+
agent-runway resolve codex --model gpt-6-astra # one answer before spawning
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add `--json` to `--models` and `resolve` for machine-readable output. Bare
|
|
23
|
+
`agent-runway` reads Claude only and is the fastest path when that is all you
|
|
24
|
+
need.
|
|
25
|
+
|
|
26
|
+
If the command is not on PATH, run it from the checkout: `node src/cli.mjs …`.
|
|
27
|
+
|
|
28
|
+
## Reading a quota answer
|
|
29
|
+
|
|
30
|
+
- A `*` marks the window **closest to its limit**, which is not the one being
|
|
31
|
+
consumed right now: a session at 3% in active use goes unmarked while an
|
|
32
|
+
untouched weekly at 87% carries it. Report it as the constraint, never as
|
|
33
|
+
"the window you are using".
|
|
34
|
+
- `not included in this plan` means the account has no entitlement to that
|
|
35
|
+
quota. It is not an exhausted quota, and must never be reported as one.
|
|
36
|
+
- A provider may come back `unreachable` or `no_credentials`. That is
|
|
37
|
+
information: say the provider could not be read rather than omitting it,
|
|
38
|
+
because silently showing three of four invites a delegation to the missing
|
|
39
|
+
one. Antigravity in particular only answers while its IDE is running.
|
|
40
|
+
|
|
41
|
+
## Reading a model answer
|
|
42
|
+
|
|
43
|
+
Every catalogue says how it was obtained, and the difference matters:
|
|
44
|
+
|
|
45
|
+
| Authority | Meaning |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `declared` | the binary was asked and answered — trust it |
|
|
48
|
+
| `inferred` | identifiers were read out of the binary — strong evidence, not a contract |
|
|
49
|
+
|
|
50
|
+
An `inferred` list can contain plausible-looking rubbish, so when a spawn is
|
|
51
|
+
built from one, be ready for it to fail anyway and say so up front.
|
|
52
|
+
|
|
53
|
+
The same agent usually has several installs — a CLI on PATH, one inside a VS
|
|
54
|
+
Code extension, one inside a desktop app — **and they disagree**. A slug the
|
|
55
|
+
editor accepts may be unknown to the binary a delegation would spawn. Always
|
|
56
|
+
name the install an answer came from.
|
|
57
|
+
|
|
58
|
+
## Before delegating to another agent
|
|
59
|
+
|
|
60
|
+
`resolve` is the single call that prevents the common failure:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
agent-runway resolve codex --model gpt-6-astra --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
It returns the binary to invoke, its version, the slugs it accepts, and for a
|
|
67
|
+
requested model: whether it is valid, which other installs would accept it, and
|
|
68
|
+
a suggested substitute. Exit 1 when the agent cannot be resolved or the slug is
|
|
69
|
+
refused, so a script can branch on it.
|
|
70
|
+
|
|
71
|
+
**Offer the substitute, never apply it silently.** Running a different model
|
|
72
|
+
than the user asked for, without saying so, is worse than failing.
|
|
73
|
+
|
|
74
|
+
## Rules
|
|
75
|
+
|
|
76
|
+
- Never print, log or copy a credential, not even a prefix. The tool never
|
|
77
|
+
emits one; do not work around that with `cat`, `echo` or a grep over
|
|
78
|
+
credential files.
|
|
79
|
+
- Never run `claude setup-token`, `codex login` or `gh auth login` on the
|
|
80
|
+
user's behalf. They open a browser and print account secrets; the user runs
|
|
81
|
+
them.
|
|
82
|
+
- Do not poll. Readings are cached for 60 seconds and the endpoints rate-limit
|
|
83
|
+
reads; a 429 there is the endpoint throttling, not the account quota.
|
|
84
|
+
- Exit 3 means a token was rejected. If the message mentions a scope, the token
|
|
85
|
+
is valid but minted for another purpose and regenerating it will not help.
|
|
86
|
+
|
|
87
|
+
## Caveat
|
|
88
|
+
|
|
89
|
+
Quota comes from endpoints that are internal and undocumented, so the shape can
|
|
90
|
+
change without notice. Model lists come from each binary, which is more stable
|
|
91
|
+
but version-specific. When something looks wrong, `--json` shows exactly what
|
|
92
|
+
was read.
|
package/src/cache.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A short-lived read cache, so repeated questions cost one request.
|
|
2
|
+
//
|
|
3
|
+
// The usage endpoints rate-limit reads: eight probes in quick succession during
|
|
4
|
+
// development were enough to earn a 429, and a 429 on a usage endpoint is
|
|
5
|
+
// especially confusing because it looks like the account quota it is meant to
|
|
6
|
+
// report on.
|
|
7
|
+
//
|
|
8
|
+
// Comparable tools sidestep this entirely by never calling an API — they read
|
|
9
|
+
// the rate_limits block Claude Code already hands a statusline on stdin. That
|
|
10
|
+
// source is free but only exists inside a statusline invocation, so an
|
|
11
|
+
// on-demand or multi-provider read still has to make a request, and still has
|
|
12
|
+
// to be careful with it.
|
|
13
|
+
//
|
|
14
|
+
// Two defences here: serve a recent answer instead of asking again, and when a
|
|
15
|
+
// provider does fail, fall back to the last good answer marked stale rather
|
|
16
|
+
// than reporting nothing. Stale numbers about a five-hour window are far more
|
|
17
|
+
// useful than silence.
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
|
|
23
|
+
const DEFAULT_TTL_MS = 60_000;
|
|
24
|
+
|
|
25
|
+
// Beyond this, a reading is not stale, it is wrong: a five-hour window has
|
|
26
|
+
// turned over several times and a weekly one has moved on. Serving it as a
|
|
27
|
+
// fallback would be worse than admitting the provider could not be reached.
|
|
28
|
+
// The same bound, and the same reasoning, as cclimits (STALE_CACHE_MAX_AGE).
|
|
29
|
+
const STALE_MAX_MS = 24 * 60 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
// Overridable so a test suite never writes fabricated readings into the cache a
|
|
32
|
+
// real run would then serve as genuine.
|
|
33
|
+
const dir = () => process.env.AGENT_RUNWAY_CACHE_DIR || path.join(os.tmpdir(), "agent-runway-cache");
|
|
34
|
+
const fileFor = (key) => path.join(dir(), `${key.replace(/[^a-z0-9_-]/gi, "_")}.json`);
|
|
35
|
+
|
|
36
|
+
/** How long a cached answer stays fresh. 0 disables the cache entirely. */
|
|
37
|
+
export function ttlMs(env = process.env) {
|
|
38
|
+
const raw = env.AGENT_RUNWAY_CACHE_MS;
|
|
39
|
+
if (raw === undefined) return DEFAULT_TTL_MS;
|
|
40
|
+
const parsed = Number(raw);
|
|
41
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_TTL_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @returns {{value: any, ageMs: number, fresh: boolean}|null}
|
|
46
|
+
*/
|
|
47
|
+
export function read(key, maxAgeMs = DEFAULT_TTL_MS, maxStaleMs = STALE_MAX_MS) {
|
|
48
|
+
try {
|
|
49
|
+
const { at, value } = JSON.parse(fs.readFileSync(fileFor(key), "utf8"));
|
|
50
|
+
const ageMs = Date.now() - at;
|
|
51
|
+
if (!Number.isFinite(ageMs) || ageMs < 0) return null;
|
|
52
|
+
// Quota readings expire with time. Anything derived from a binary does not:
|
|
53
|
+
// it changes when the file changes, which the key already encodes, so those
|
|
54
|
+
// callers pass Infinity rather than a duration.
|
|
55
|
+
if (ageMs > maxStaleMs) return null;
|
|
56
|
+
return { value, ageMs, fresh: maxAgeMs > 0 && ageMs <= maxAgeMs };
|
|
57
|
+
} catch {
|
|
58
|
+
return null; // absent or unreadable is simply a miss
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function write(key, value) {
|
|
63
|
+
try {
|
|
64
|
+
fs.mkdirSync(dir(), { recursive: true });
|
|
65
|
+
fs.writeFileSync(fileFor(key), JSON.stringify({ at: Date.now(), value }), "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
// A cache that cannot be written must never break the read it was helping.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function clear(key) {
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(key ? fileFor(key) : dir(), { recursive: true, force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
/* nothing to clear */
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Seconds a 429 asks us to wait, when the server says so.
|
|
81
|
+
* Retry-After is either a delay in seconds or an HTTP date.
|
|
82
|
+
*/
|
|
83
|
+
export function retryAfterSeconds(headers) {
|
|
84
|
+
const raw = headers?.get?.("retry-after");
|
|
85
|
+
if (!raw) return null;
|
|
86
|
+
const seconds = Number(raw);
|
|
87
|
+
if (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds));
|
|
88
|
+
const when = Date.parse(raw);
|
|
89
|
+
return Number.isFinite(when) ? Math.max(0, Math.round((when - Date.now()) / 1000)) : null;
|
|
90
|
+
}
|