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.
@@ -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
+ }