fest-gateway 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/cli/claude.js +326 -0
- package/dist/cli/claude.js.map +1 -0
- package/dist/cli/config.js +59 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/login.js +130 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/logout.js +20 -0
- package/dist/cli/logout.js.map +1 -0
- package/dist/cli/whoami.js +34 -0
- package/dist/cli/whoami.js.map +1 -0
- package/dist/server/adapters/anthropic.js +74 -0
- package/dist/server/adapters/anthropic.js.map +1 -0
- package/dist/server/adapters/fireworks.js +54 -0
- package/dist/server/adapters/fireworks.js.map +1 -0
- package/dist/server/adapters/index.js +16 -0
- package/dist/server/adapters/index.js.map +1 -0
- package/dist/server/adapters/registry.js +38 -0
- package/dist/server/adapters/registry.js.map +1 -0
- package/dist/server/adapters/rewrite.js +152 -0
- package/dist/server/adapters/rewrite.js.map +1 -0
- package/dist/server/adapters/url.js +24 -0
- package/dist/server/adapters/url.js.map +1 -0
- package/dist/server/api/auth.js +213 -0
- package/dist/server/api/auth.js.map +1 -0
- package/dist/server/api/live.js +108 -0
- package/dist/server/api/live.js.map +1 -0
- package/dist/server/api/models.js +129 -0
- package/dist/server/api/models.js.map +1 -0
- package/dist/server/api/oauth.js +300 -0
- package/dist/server/api/oauth.js.map +1 -0
- package/dist/server/api/routes.js +164 -0
- package/dist/server/api/routes.js.map +1 -0
- package/dist/server/api/routing-view.js +47 -0
- package/dist/server/api/routing-view.js.map +1 -0
- package/dist/server/auth/accounts.js +111 -0
- package/dist/server/auth/accounts.js.map +1 -0
- package/dist/server/auth/gateway-401.js +85 -0
- package/dist/server/auth/gateway-401.js.map +1 -0
- package/dist/server/auth/guard.js +74 -0
- package/dist/server/auth/guard.js.map +1 -0
- package/dist/server/auth/oauth.js +43 -0
- package/dist/server/auth/oauth.js.map +1 -0
- package/dist/server/auth/password.js +104 -0
- package/dist/server/auth/password.js.map +1 -0
- package/dist/server/auth/posture.js +145 -0
- package/dist/server/auth/posture.js.map +1 -0
- package/dist/server/auth/session.js +161 -0
- package/dist/server/auth/session.js.map +1 -0
- package/dist/server/bin/fest.js +589 -0
- package/dist/server/bin/fest.js.map +1 -0
- package/dist/server/config.js +109 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/credentials/provider.js +70 -0
- package/dist/server/credentials/provider.js.map +1 -0
- package/dist/server/credentials/resolve.js +89 -0
- package/dist/server/credentials/resolve.js.map +1 -0
- package/dist/server/http/body.js +66 -0
- package/dist/server/http/body.js.map +1 -0
- package/dist/server/http/errors.js +144 -0
- package/dist/server/http/errors.js.map +1 -0
- package/dist/server/http/headers.js +189 -0
- package/dist/server/http/headers.js.map +1 -0
- package/dist/server/http/pipe.js +97 -0
- package/dist/server/http/pipe.js.map +1 -0
- package/dist/server/http/server.js +226 -0
- package/dist/server/http/server.js.map +1 -0
- package/dist/server/http/sse.js +167 -0
- package/dist/server/http/sse.js.map +1 -0
- package/dist/server/http/static.js +138 -0
- package/dist/server/http/static.js.map +1 -0
- package/dist/server/ingest/live-bus.js +67 -0
- package/dist/server/ingest/live-bus.js.map +1 -0
- package/dist/server/ingest/sink.js +141 -0
- package/dist/server/ingest/sink.js.map +1 -0
- package/dist/server/log.js +31 -0
- package/dist/server/log.js.map +1 -0
- package/dist/server/pipeline/count-tokens.js +48 -0
- package/dist/server/pipeline/count-tokens.js.map +1 -0
- package/dist/server/pipeline/dispatch.js +138 -0
- package/dist/server/pipeline/dispatch.js.map +1 -0
- package/dist/server/pipeline/passthrough.js +392 -0
- package/dist/server/pipeline/passthrough.js.map +1 -0
- package/dist/server/pipeline/preflight.js +66 -0
- package/dist/server/pipeline/preflight.js.map +1 -0
- package/dist/server/pipeline/record.js +49 -0
- package/dist/server/pipeline/record.js.map +1 -0
- package/dist/server/pipeline/substitute.js +287 -0
- package/dist/server/pipeline/substitute.js.map +1 -0
- package/dist/server/routes/resolve.js +176 -0
- package/dist/server/routes/resolve.js.map +1 -0
- package/dist/server/routes/table.js +247 -0
- package/dist/server/routes/table.js.map +1 -0
- package/dist/server/routes/watch.js +101 -0
- package/dist/server/routes/watch.js.map +1 -0
- package/dist/server/secret/fingerprint.js +107 -0
- package/dist/server/secret/fingerprint.js.map +1 -0
- package/dist/server/secret/non-persistable.js +69 -0
- package/dist/server/secret/non-persistable.js.map +1 -0
- package/dist/server/store/audit.js +53 -0
- package/dist/server/store/audit.js.map +1 -0
- package/dist/server/store/bootstrap.js +53 -0
- package/dist/server/store/bootstrap.js.map +1 -0
- package/dist/server/store/db.js +157 -0
- package/dist/server/store/db.js.map +1 -0
- package/dist/server/store/ids.js +14 -0
- package/dist/server/store/ids.js.map +1 -0
- package/dist/server/store/migrations/001-init.sql +213 -0
- package/dist/server/store/migrations/002-feed-and-error-indexes.sql +26 -0
- package/dist/server/store/migrations/003-routing.sql +42 -0
- package/dist/server/store/migrations/004-admin-auth.sql +72 -0
- package/dist/server/store/migrations/005-notional-cost.sql +28 -0
- package/dist/server/store/queries.js +904 -0
- package/dist/server/store/queries.js.map +1 -0
- package/dist/server/store/retention.js +79 -0
- package/dist/server/store/retention.js.map +1 -0
- package/dist/server/store/seed.js +203 -0
- package/dist/server/store/seed.js.map +1 -0
- package/dist/server/store/tokens.js +133 -0
- package/dist/server/store/tokens.js.map +1 -0
- package/dist/server/store/write.js +330 -0
- package/dist/server/store/write.js.map +1 -0
- package/dist/server/usage/accumulator.js +209 -0
- package/dist/server/usage/accumulator.js.map +1 -0
- package/dist/server/usage/cost.js +57 -0
- package/dist/server/usage/cost.js.map +1 -0
- package/dist/server/usage/prices/catalog.js +136 -0
- package/dist/server/usage/prices/catalog.js.map +1 -0
- package/dist/server/usage/prices/refresh.js +125 -0
- package/dist/server/usage/prices/refresh.js.map +1 -0
- package/dist/server/usage/prices/snapshot.json.gz +0 -0
- package/dist/server/usage/prices/table.js +102 -0
- package/dist/server/usage/prices/table.js.map +1 -0
- package/dist/server/usage/pricing.js +103 -0
- package/dist/server/usage/pricing.js.map +1 -0
- package/dist/shared/api.js +19 -0
- package/dist/shared/api.js.map +1 -0
- package/dist/shared/base-models.js +28 -0
- package/dist/shared/base-models.js.map +1 -0
- package/dist/shared/demotion-vars.js +17 -0
- package/dist/shared/demotion-vars.js.map +1 -0
- package/dist/shared/series.js +50 -0
- package/dist/shared/series.js.map +1 -0
- package/dist/shared/types.js +40 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/web/dist/assets/index-8HlKsJqt.css +2 -0
- package/dist/web/dist/assets/index-DQ7fyPVm.js +56 -0
- package/dist/web/dist/assets/index-DQ7fyPVm.js.map +1 -0
- package/dist/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist/web/dist/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist/web/dist/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist/web/dist/index.html +44 -0
- package/dist/web/dist/nearform-icon.svg +4 -0
- package/package.json +82 -0
- package/routes.example.json +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clifton Cunningham
|
|
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,274 @@
|
|
|
1
|
+
# Fest
|
|
2
|
+
|
|
3
|
+
A self-hosted gateway for Claude Code. Teams point their Claude Code at Fest the
|
|
4
|
+
way they'd point at Bedrock, so one place can monitor requests, tokens and cost —
|
|
5
|
+
and route to different models without Claude Code knowing the difference.
|
|
6
|
+
|
|
7
|
+
**The distinguishing goal: Claude Max / Team subscription support.** Each
|
|
8
|
+
developer authenticates with their *own* subscription. Fest relays their own
|
|
9
|
+
credential and never stores it. Per-user pass-through, never pooling.
|
|
10
|
+
|
|
11
|
+
> **Status: feature complete for a team trial.** A real Claude Code session on a
|
|
12
|
+
> Max subscription runs through Fest, is attributed to its developer, metered
|
|
13
|
+
> into SQLite, and shown on a dashboard behind a login. Model routing and
|
|
14
|
+
> substitution work and are visible. Phase 0 cleared both gates
|
|
15
|
+
> ([results](docs/PHASE0-RESULTS.md)), and `npm run canary` re-checks them
|
|
16
|
+
> against each new Claude Code release.
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
docker compose run --rm fest admin create you@corp.test # password shown once
|
|
20
|
+
docker compose up -d
|
|
21
|
+
docker compose run --rm fest token create dev@corp.test laptop # shown once
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each developer then points Claude Code at the printed URL:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
env -u ANTHROPIC_API_KEY -u ANTHROPIC_AUTH_TOKEN \
|
|
28
|
+
ANTHROPIC_BASE_URL=http://fest.corp:8787/t/<token> claude
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or, if the operator has configured Google/GitHub OAuth (see below), a developer
|
|
32
|
+
can skip all of that and self-serve a token from their own machine:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g fest-gateway # the command it installs is `fest`
|
|
36
|
+
|
|
37
|
+
fest login --server http://fest.corp:8787 # opens a browser, stores a token in ~/.fest
|
|
38
|
+
fest claude # runs `claude`, env set up for you
|
|
39
|
+
fest whoami # what's logged in, and whether it's still live
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package is `fest-gateway` — plain `fest` was already taken on npm — but the
|
|
43
|
+
binary it installs is `fest`. Without installing anything, `npx fest-gateway
|
|
44
|
+
login …` does the same job.
|
|
45
|
+
|
|
46
|
+
From a clone of this repo the CLI runs straight off the TypeScript instead:
|
|
47
|
+
`npm run cli -- login …` (note the `--`), or `node server/bin/fest.ts login …`.
|
|
48
|
+
|
|
49
|
+
Or without Docker:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
node server/bin/fest.ts admin create you@corp.test
|
|
53
|
+
node server/bin/fest.ts token create dev@corp.test laptop
|
|
54
|
+
FEST_REQUIRE_IDENTITY=1 node server/bin/fest.ts serve
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Why this is possible
|
|
58
|
+
|
|
59
|
+
Claude Code's inference client attaches the subscription OAuth bearer with **no
|
|
60
|
+
host check** against `ANTHROPIC_BASE_URL`. Point it at Fest while logged in with
|
|
61
|
+
Max/Team and Fest receives `Authorization: Bearer sk-ant-oat…`. Confirmed both
|
|
62
|
+
by reading the binary and by running it — see
|
|
63
|
+
[docs/PHASE0-RESULTS.md](docs/PHASE0-RESULTS.md).
|
|
64
|
+
|
|
65
|
+
Token refresh happens client-side against `api.anthropic.com`, outside
|
|
66
|
+
`ANTHROPIC_BASE_URL`. So Fest **forwards and forgets**: it never stores,
|
|
67
|
+
refreshes or holds a subscription credential. That is a genuine security
|
|
68
|
+
advantage over any "store the team's key" design, not just a side effect.
|
|
69
|
+
|
|
70
|
+
## Two things that will bite you
|
|
71
|
+
|
|
72
|
+
**1. `ANTHROPIC_API_KEY` and `ANTHROPIC_AUTH_TOKEN` silently kill the
|
|
73
|
+
subscription.** Claude Code demotes off OAuth if *any* Anthropic-issued
|
|
74
|
+
credential is present — including an `apiKeyHelper`. So Fest's identity token
|
|
75
|
+
never goes in those variables; it rides in the URL path prefix or a custom
|
|
76
|
+
header. A design that ignores this silently moves a whole team off their
|
|
77
|
+
subscriptions onto a server-held key, and the failure is invisible.
|
|
78
|
+
|
|
79
|
+
**2. This is supported-by-absence, not by design.** There is no *documented*
|
|
80
|
+
path for relaying a subscription bearer through a third-party gateway. It works
|
|
81
|
+
because no guard exists. Treat it as a version-pinned dependency, and settle the
|
|
82
|
+
policy question with Anthropic before a team-wide rollout.
|
|
83
|
+
|
|
84
|
+
The version dependency is enforced rather than remembered: `npm run canary`
|
|
85
|
+
re-runs both load-bearing Phase 0 experiments against the installed release and
|
|
86
|
+
refuses the rollout if either has changed. See [docs/CANARY.md](docs/CANARY.md);
|
|
87
|
+
verdicts are recorded per version in `docs/canary-history.jsonl`.
|
|
88
|
+
|
|
89
|
+
Worth knowing: Anthropic ships its own self-hostable `claude gateway` (see
|
|
90
|
+
`claude gateway --help`) with central metering, pricing and managed policies. It
|
|
91
|
+
authenticates via OIDC and holds provider credentials itself, so it cannot do
|
|
92
|
+
subscription pass-through — but if that requirement ever softens, it is the
|
|
93
|
+
supported product.
|
|
94
|
+
|
|
95
|
+
## Design commitments
|
|
96
|
+
|
|
97
|
+
- **Byte-for-byte pass-through.** Subscription tokens are validated against
|
|
98
|
+
request shape, so the body is relayed as opaque bytes — never parsed and
|
|
99
|
+
re-serialised. No model rewriting or prompt shaping on that path.
|
|
100
|
+
- **`cache_control` forwarded verbatim.** Stripping it silently destroys prompt
|
|
101
|
+
caching and inflates every developer's token bill.
|
|
102
|
+
- **Metadata only.** Token counts, models, latency, status — never prompt or
|
|
103
|
+
completion content.
|
|
104
|
+
- **No credential is persistable.** Not by policy, by construction: there is no
|
|
105
|
+
store to put one in. Only fingerprints, enforced by tests.
|
|
106
|
+
|
|
107
|
+
## Layout
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
server/bin/fest.ts CLI: serve | migrate | token | admin | seed |
|
|
111
|
+
login | whoami | claude | logout
|
|
112
|
+
server/pipeline/ passthrough (byte-for-byte) — the subscription path
|
|
113
|
+
server/http/ sse parser, tee, header discipline, errors, routing
|
|
114
|
+
server/store/ SQLite schema, write path, identity tokens
|
|
115
|
+
server/usage/ accumulator, lean pricing, nullable cost algebra
|
|
116
|
+
server/secret/ credential classification + redaction
|
|
117
|
+
server/auth/oauth.ts Google/GitHub provider construction
|
|
118
|
+
server/api/oauth.ts /api/auth/oauth/* and /api/auth/identity
|
|
119
|
+
cli/ the developer half of the CLI — no DB access, ever.
|
|
120
|
+
login (loopback OAuth flow) | whoami | claude | logout,
|
|
121
|
+
and the ~/.fest config they share
|
|
122
|
+
shared/types.ts the contracts both halves agree on
|
|
123
|
+
shared/demotion-vars.ts env vars that silently kill subscription auth
|
|
124
|
+
tools/capture-server.ts Phase 0 diagnostic (throwaway)
|
|
125
|
+
docs/PHASE0.md the gating experiment runbook
|
|
126
|
+
docs/CLI-AUTH.md OAuth setup and the fest login/whoami/claude flow
|
|
127
|
+
test/ node --test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Accounting rules that are easy to get wrong
|
|
131
|
+
|
|
132
|
+
- **The four token buckets are disjoint.** `input_tokens` excludes cache reads
|
|
133
|
+
and cache writes; context size is the sum. Folding cache reads into input is
|
|
134
|
+
an order-of-magnitude error on a cache-heavy agent workload — and Claude Code
|
|
135
|
+
is one (a trivial `-p` call here showed ~16k cache reads and ~12k one-hour
|
|
136
|
+
cache writes).
|
|
137
|
+
- **`null` cost means unavailable, never zero.** A subscription request is real
|
|
138
|
+
usage with *no org spend*; an unknown model is unpriced, not free. Rollups
|
|
139
|
+
therefore carry a priced sum plus `unpriced_requests` and
|
|
140
|
+
`subscription_requests` separately, so a total renders honestly as
|
|
141
|
+
`$12.3456 (+3 n/a)`.
|
|
142
|
+
- **For a subscription developer, quota is the scarce resource, not dollars.**
|
|
143
|
+
Anthropic returns 5h/7d utilisation on every response, so that is what the
|
|
144
|
+
dashboard shows rather than an invented dollar figure.
|
|
145
|
+
|
|
146
|
+
## What Fest can see, and what it keeps
|
|
147
|
+
|
|
148
|
+
This is a gateway every prompt a team writes passes through, so it is worth
|
|
149
|
+
being exact about what that means rather than reassuring.
|
|
150
|
+
|
|
151
|
+
**Fest sees everything, because it has to.** A request body arrives at this
|
|
152
|
+
process in full: prompts, file contents you asked Claude about, tool results,
|
|
153
|
+
the lot. That is unavoidable for anything sitting on the wire, and no wording
|
|
154
|
+
changes it.
|
|
155
|
+
|
|
156
|
+
**Fest keeps none of it.** Request bodies are relayed as opaque bytes and never
|
|
157
|
+
parsed for storage. What is written down is metadata: who, when, which model,
|
|
158
|
+
how many tokens in each of the four buckets, latency, HTTP status, error type,
|
|
159
|
+
and the session id Claude Code already sends. There is no column anywhere in
|
|
160
|
+
the schema for prompt or completion text — not disabled, absent — so "turn off
|
|
161
|
+
content capture" is not a setting that could be misconfigured.
|
|
162
|
+
|
|
163
|
+
**No credential is persistable, by construction rather than by policy.** A
|
|
164
|
+
subscription bearer is forwarded and forgotten; there is no table to put one in.
|
|
165
|
+
What is stored of a Fest identity token or a dashboard password is a hash. The
|
|
166
|
+
leak tests grep the entire database, every log line and every API response for
|
|
167
|
+
secret-shaped strings.
|
|
168
|
+
|
|
169
|
+
**Raw per-request rows age out after 30 days**; the hourly rollups, which are
|
|
170
|
+
aggregate, are kept for 400 so a year of trends does not require a year of
|
|
171
|
+
per-developer detail.
|
|
172
|
+
|
|
173
|
+
**An admin can see who used what, and when.** That is the point of a team
|
|
174
|
+
gateway, and the developers pointing their editors at it should be told so
|
|
175
|
+
plainly. They cannot see each other: a `member` account sees only its own
|
|
176
|
+
traffic, enforced in the query layer rather than in the UI.
|
|
177
|
+
|
|
178
|
+
## Deploying it
|
|
179
|
+
|
|
180
|
+
`docker compose up -d` after creating an owner account. The image has no runtime
|
|
181
|
+
dependencies — Node runs the TypeScript directly — so it is Node plus this
|
|
182
|
+
repository.
|
|
183
|
+
|
|
184
|
+
The dashboard needs a sign-in: scrypt passwords, an opaque session cookie
|
|
185
|
+
hashed at rest, an origin check on the two endpoints that mutate anything, and
|
|
186
|
+
an audit log of administrative actions. Accounts are granted from the host with
|
|
187
|
+
`fest admin create`; there is no self-registration and no email reset, because a
|
|
188
|
+
gateway should not need mail credentials to run.
|
|
189
|
+
|
|
190
|
+
Before anyone has run `fest admin create`, Fest serves the dashboard **open on
|
|
191
|
+
loopback and refuses to start on any other interface**. A single-developer trial
|
|
192
|
+
needs no setup, and "bound it to 0.0.0.0 and forgot the password" cannot happen
|
|
193
|
+
quietly.
|
|
194
|
+
|
|
195
|
+
Put it behind your own TLS proxy and set `FEST_SECURE_COOKIES=1` when you do:
|
|
196
|
+
the server only ever sees plain HTTP and cannot detect this for itself.
|
|
197
|
+
|
|
198
|
+
| | |
|
|
199
|
+
| --- | --- |
|
|
200
|
+
| `FEST_PORT`, `FEST_HOST` | listen address. Default `127.0.0.1:8787`. |
|
|
201
|
+
| `FEST_DB`, `FEST_USAGE_LOG` | SQLite path, and the greppable JSONL trail. |
|
|
202
|
+
| `FEST_REQUIRE_IDENTITY` | refuse unattributed requests. Off by default; **on** for a team. |
|
|
203
|
+
| `FEST_SECURE_COOKIES` | set behind HTTPS. |
|
|
204
|
+
| `FEST_ROUTES` | routing table path. Absent means everything passes through. |
|
|
205
|
+
| `FEST_UPSTREAM_BASE_URL`, `FEST_LOG_LEVEL` | |
|
|
206
|
+
| `FEST_PRICING_REFRESH` | set `0` to never fetch prices. Rates then come only from the vendored snapshot, which is always sufficient. |
|
|
207
|
+
| `FEST_PRICING_URL` | internal mirror of litellm's price file. |
|
|
208
|
+
| `FEST_GOOGLE_CLIENT_ID`, `FEST_GOOGLE_CLIENT_SECRET` | Google OAuth, for `fest login` and the dashboard. Absent means Google sign-in is off. |
|
|
209
|
+
| `FEST_GITHUB_CLIENT_ID`, `FEST_GITHUB_CLIENT_SECRET` | GitHub OAuth, same idea. |
|
|
210
|
+
| `FEST_ALLOWED_EMAIL_DOMAINS` | comma-separated. **Required to enable OAuth at all** — empty refuses every OAuth login rather than allowing any Google/GitHub account. |
|
|
211
|
+
| `FEST_PUBLIC_URL` | base URL Fest is reachable at, for building the OAuth redirect URI. Defaults to `http://<host>:<port>`. |
|
|
212
|
+
| `FEST_BASE_PATH` | mount path, if Fest is not at the root of its origin. Defaults to the path of `FEST_PUBLIC_URL`, so setting that is usually enough. |
|
|
213
|
+
|
|
214
|
+
See [docs/CLI-AUTH.md](docs/CLI-AUTH.md) for the full OAuth setup and how
|
|
215
|
+
`fest login`'s loopback flow works.
|
|
216
|
+
|
|
217
|
+
### Behind Caddy, on a path
|
|
218
|
+
|
|
219
|
+
Fest can be mounted under a path rather than on its own hostname —
|
|
220
|
+
`https://lastlight.nearform.dev/fest`. Set `FEST_PUBLIC_URL` to the full URL
|
|
221
|
+
including that path and Fest takes the mount point from it: the dashboard's
|
|
222
|
+
`<base href>` is rewritten to match, and the OAuth redirect URI is built
|
|
223
|
+
against it.
|
|
224
|
+
|
|
225
|
+
```caddyfile
|
|
226
|
+
lastlight.nearform.dev {
|
|
227
|
+
handle_path /fest/* {
|
|
228
|
+
reverse_proxy 127.0.0.1:8787 {
|
|
229
|
+
# SSE: /api/live is a long-lived event stream, and a buffering
|
|
230
|
+
# proxy turns the live screen into a screen that updates once.
|
|
231
|
+
flush_interval -1
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
FEST_PUBLIC_URL=https://lastlight.nearform.dev/fest
|
|
239
|
+
FEST_SECURE_COOKIES=1
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
`handle_path` strips the `/fest` prefix before proxying. A plain `handle` +
|
|
243
|
+
`reverse_proxy`, which does not strip it, works too — Fest removes its own
|
|
244
|
+
mount path from an incoming request when it is still there, so either spelling
|
|
245
|
+
routes. Caddy passes the original `Host` upstream by default, which the CSRF
|
|
246
|
+
origin check needs; on nginx that is `proxy_set_header Host $host`, and
|
|
247
|
+
`proxy_buffering off` for the event stream.
|
|
248
|
+
|
|
249
|
+
The registered OAuth redirect URI must include the path —
|
|
250
|
+
`https://lastlight.nearform.dev/fest/api/auth/oauth/google/callback`. Developers
|
|
251
|
+
point both Claude Code and the CLI at the full prefixed URL
|
|
252
|
+
(`fest login --server https://lastlight.nearform.dev/fest`).
|
|
253
|
+
|
|
254
|
+
## Requirements
|
|
255
|
+
|
|
256
|
+
Node >= 22.18 (TypeScript runs directly via native type stripping — no build
|
|
257
|
+
step for the server). Vite + React are used only for the dashboard, later.
|
|
258
|
+
|
|
259
|
+
## Commands
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
npm test # node --test
|
|
263
|
+
npm run canary # version gate: re-run before rolling out a Claude Code release
|
|
264
|
+
npm run capture # Phase 0 capture server
|
|
265
|
+
npm run typecheck # tsc --noEmit (needs npm install first)
|
|
266
|
+
npm run dev # gateway + dashboard with hot reload
|
|
267
|
+
npm run demo # synthetic traffic in a scratch database, then both of the above
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
node server/bin/fest.ts admin create <email> [--role R] [--password-stdin]
|
|
272
|
+
node server/bin/fest.ts admin list | admin passwd <email> | admin disable <email>
|
|
273
|
+
node server/bin/fest.ts token create <email> [name] | token list | token revoke <id>
|
|
274
|
+
```
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fest claude`: run the real `claude` binary, pointed at this developer's
|
|
3
|
+
* Fest gateway, in whichever of two postures will actually work here.
|
|
4
|
+
*
|
|
5
|
+
* **Subscription posture** sets `ANTHROPIC_BASE_URL` to the path-prefixed
|
|
6
|
+
* identity carrier (`server/auth/posture.ts`'s `parseIdentityPath`) and sets no
|
|
7
|
+
* credential, so Claude Code's own OAuth login pays. **Gateway posture** sets a
|
|
8
|
+
* bare base URL plus `ANTHROPIC_AUTH_TOKEN=<fest token>`, so the server's own
|
|
9
|
+
* credentials pay and the server-published `/model` menu becomes reachable.
|
|
10
|
+
*
|
|
11
|
+
* The two are mutually exclusive in the client: gateway model discovery needs a
|
|
12
|
+
* credential in the environment, and any such credential disables subscription
|
|
13
|
+
* auth. So Fest picks — OAuth login present → subscription, absent → gateway —
|
|
14
|
+
* and `fest claude gw` forces gateway when a developer wants the menu anyway.
|
|
15
|
+
*
|
|
16
|
+
* Both postures strip every demotion var first — the same reasoning
|
|
17
|
+
* `tools/canary.ts` applies to its test client, from the same
|
|
18
|
+
* `shared/demotion-vars.ts` list. Gateway posture then sets its token *after*
|
|
19
|
+
* that strip rather than exempting it, so an inherited `ANTHROPIC_API_KEY` can
|
|
20
|
+
* never survive alongside the Fest token.
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { DEMOTION_VARS } from "../shared/demotion-vars.js";
|
|
27
|
+
import { BASE_MODELS } from "../shared/base-models.js";
|
|
28
|
+
import { readCliConfig } from "./config.js";
|
|
29
|
+
/**
|
|
30
|
+
* Pure, so it's testable without spawning a real `claude` binary.
|
|
31
|
+
*
|
|
32
|
+
* Defaults to subscription posture: that is what `fest claude` did before there
|
|
33
|
+
* was a choice, and it remains the answer for a developer with their own login.
|
|
34
|
+
*/
|
|
35
|
+
export function buildClaudeEnv(base, cfg, posture = "subscription") {
|
|
36
|
+
const env = { ...base };
|
|
37
|
+
for (const v of DEMOTION_VARS)
|
|
38
|
+
delete env[v];
|
|
39
|
+
if (posture === "key") {
|
|
40
|
+
env["ANTHROPIC_BASE_URL"] = cfg.serverUrl;
|
|
41
|
+
// Set after the strip, never by exempting it from the strip list.
|
|
42
|
+
env["ANTHROPIC_AUTH_TOKEN"] = cfg.identityToken;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
env["ANTHROPIC_BASE_URL"] = `${cfg.serverUrl}/t/${cfg.identityToken}`;
|
|
46
|
+
}
|
|
47
|
+
return env;
|
|
48
|
+
}
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Detection — metadata and keys only, never a credential value
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
/** The four layers Claude Code merges settings from, most general first. */
|
|
53
|
+
function settingsFiles() {
|
|
54
|
+
return [
|
|
55
|
+
join(homedir(), ".claude", "settings.json"),
|
|
56
|
+
join(homedir(), ".claude", "settings.local.json"),
|
|
57
|
+
join(process.cwd(), ".claude", "settings.json"),
|
|
58
|
+
join(process.cwd(), ".claude", "settings.local.json"),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
function readSettings(path) {
|
|
62
|
+
if (!existsSync(path))
|
|
63
|
+
return null;
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return "unparseable";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Is this machine on an Anthropic OAuth subscription login?
|
|
73
|
+
*
|
|
74
|
+
* Read exactly as `tools/canary.ts::subscriptionLogin()` reads it — account
|
|
75
|
+
* metadata only. No credential lives in this function, and the reason string is
|
|
76
|
+
* printed to a developer's terminal, so nothing identifying goes in it either.
|
|
77
|
+
*/
|
|
78
|
+
export function detectSubscriptionLogin() {
|
|
79
|
+
const path = join(homedir(), ".claude.json");
|
|
80
|
+
if (!existsSync(path))
|
|
81
|
+
return { ok: false, detail: "no Anthropic subscription login found" };
|
|
82
|
+
const parsed = readSettings(path);
|
|
83
|
+
if (parsed === null || parsed === "unparseable") {
|
|
84
|
+
return { ok: false, detail: "~/.claude.json is unparseable" };
|
|
85
|
+
}
|
|
86
|
+
if (!parsed["oauthAccount"]) {
|
|
87
|
+
return { ok: false, detail: "no Anthropic subscription login found" };
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, detail: "Anthropic OAuth login found" };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Settings-layer demotion triggers, by KEY only.
|
|
93
|
+
*
|
|
94
|
+
* `buildClaudeEnv` can strip a poisoned env var; it cannot touch these, because
|
|
95
|
+
* the client reads its own settings files. In subscription posture that is the
|
|
96
|
+
* silent failure this whole project exists to prevent: the developer believes
|
|
97
|
+
* they are on their own plan, and the session is quietly billed to someone
|
|
98
|
+
* else's key. Values are never read — `env.ANTHROPIC_API_KEY` in a settings
|
|
99
|
+
* file is a live secret.
|
|
100
|
+
*/
|
|
101
|
+
export function settingsDemotionTriggers() {
|
|
102
|
+
const found = [];
|
|
103
|
+
for (const path of settingsFiles()) {
|
|
104
|
+
const parsed = readSettings(path);
|
|
105
|
+
if (parsed === null)
|
|
106
|
+
continue;
|
|
107
|
+
if (parsed === "unparseable") {
|
|
108
|
+
found.push(`${path}: unparseable, cannot check it`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (parsed["apiKeyHelper"])
|
|
112
|
+
found.push(`${path}: apiKeyHelper`);
|
|
113
|
+
for (const v of DEMOTION_VARS) {
|
|
114
|
+
if (parsed["env"] && v in parsed["env"])
|
|
115
|
+
found.push(`${path}: env.${v}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The model the session will open on, as far as the CLI can observe it.
|
|
122
|
+
*
|
|
123
|
+
* Precedence follows the client's own: an explicit `--model` beats
|
|
124
|
+
* `ANTHROPIC_MODEL`, which beats a `model` key in the settings layers (most
|
|
125
|
+
* specific layer wins). `null` means *unknown* — Claude Code's built-in default
|
|
126
|
+
* or the developer's last selection, neither of which is visible from here.
|
|
127
|
+
*/
|
|
128
|
+
export function resolveIntendedModel(argv, env) {
|
|
129
|
+
for (let i = 0; i < argv.length; i++) {
|
|
130
|
+
const arg = argv[i];
|
|
131
|
+
if (arg === "--model" && i + 1 < argv.length)
|
|
132
|
+
return argv[i + 1];
|
|
133
|
+
if (arg.startsWith("--model="))
|
|
134
|
+
return arg.slice("--model=".length);
|
|
135
|
+
}
|
|
136
|
+
const fromEnv = env["ANTHROPIC_MODEL"];
|
|
137
|
+
if (fromEnv !== undefined && fromEnv !== "")
|
|
138
|
+
return fromEnv;
|
|
139
|
+
let fromSettings = null;
|
|
140
|
+
for (const path of settingsFiles()) {
|
|
141
|
+
const parsed = readSettings(path);
|
|
142
|
+
if (parsed === null || parsed === "unparseable")
|
|
143
|
+
continue;
|
|
144
|
+
if (typeof parsed["model"] === "string" && parsed["model"] !== "")
|
|
145
|
+
fromSettings = parsed["model"];
|
|
146
|
+
}
|
|
147
|
+
return fromSettings;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Does the model this session will open on have a substitute route?
|
|
151
|
+
*
|
|
152
|
+
* "The menu is non-empty" is not the question. In gateway posture there is no
|
|
153
|
+
* caller credential to fall back on, so an unrouted model 401s on the first
|
|
154
|
+
* message — and against a typical routing table the menu is full while Opus and
|
|
155
|
+
* Haiku are both unrouted. This turns that first-message 401 into a sentence
|
|
156
|
+
* read before the session starts.
|
|
157
|
+
*
|
|
158
|
+
* Pure: the fetch lives in `fetchServableModels`, so the cliff logic is
|
|
159
|
+
* testable without a socket.
|
|
160
|
+
*/
|
|
161
|
+
export function checkServable(menu, intended) {
|
|
162
|
+
const servable = [...menu].sort();
|
|
163
|
+
const list = servable.join(", ");
|
|
164
|
+
if (intended !== null) {
|
|
165
|
+
if (servable.includes(intended))
|
|
166
|
+
return { kind: "ok", message: "" };
|
|
167
|
+
return {
|
|
168
|
+
kind: "fatal",
|
|
169
|
+
message: `${intended} has no substitute route on this gateway; in gateway posture it would fail on the first message.\n` +
|
|
170
|
+
`Servable: ${list}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// Unknown: the client will open on its own default or last selection. Name
|
|
174
|
+
// precisely which of Anthropic's base models would fail if it picks one.
|
|
175
|
+
const missing = BASE_MODELS.filter((m) => !servable.includes(m.id));
|
|
176
|
+
if (missing.length === 0)
|
|
177
|
+
return { kind: "ok", message: "" };
|
|
178
|
+
const names = missing.map((m) => m.name);
|
|
179
|
+
const subject = names.length === 1 ? `${names[0]} is` : `${names.slice(0, -1).join(", ")} and ${names.at(-1)} are`;
|
|
180
|
+
const object = names.length === 1 ? "it" : names.length === 2 ? "either" : "any of them";
|
|
181
|
+
return {
|
|
182
|
+
kind: "warn",
|
|
183
|
+
message: `${subject} not routed on this gateway; selecting ${object} will fail.\nServable: ${list}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** How long to wait for the menu. The client's own discovery timeout is 3s. */
|
|
187
|
+
const MENU_TIMEOUT_MS = 3_000;
|
|
188
|
+
/**
|
|
189
|
+
* `GET /v1/models` — served without identity resolution, so no auth header is
|
|
190
|
+
* needed here.
|
|
191
|
+
*
|
|
192
|
+
* A 404 or an empty list is fatal: gateway posture with no substitute routes
|
|
193
|
+
* cannot serve anything. A network error is only a warning — a flaky link must
|
|
194
|
+
* not brick a known-good gateway.
|
|
195
|
+
*/
|
|
196
|
+
export async function fetchServableModels(serverUrl) {
|
|
197
|
+
let res;
|
|
198
|
+
try {
|
|
199
|
+
res = await fetch(`${serverUrl}/v1/models?limit=1000`, {
|
|
200
|
+
signal: AbortSignal.timeout(MENU_TIMEOUT_MS),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
return {
|
|
205
|
+
ok: false,
|
|
206
|
+
fatal: false,
|
|
207
|
+
message: `could not reach ${serverUrl}/v1/models (${err instanceof Error ? err.message : String(err)}) — continuing unchecked.`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (res.status === 404) {
|
|
211
|
+
return {
|
|
212
|
+
ok: false,
|
|
213
|
+
fatal: true,
|
|
214
|
+
message: `${serverUrl} publishes no gateway models, so gateway posture has nothing to serve.\n` +
|
|
215
|
+
"The gateway needs substitute routes — check FEST_ROUTES on the server.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (!res.ok) {
|
|
219
|
+
return { ok: false, fatal: false, message: `GET /v1/models returned ${res.status} — continuing unchecked.` };
|
|
220
|
+
}
|
|
221
|
+
let body;
|
|
222
|
+
try {
|
|
223
|
+
body = (await res.json());
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return { ok: false, fatal: false, message: "GET /v1/models returned unparseable JSON — continuing unchecked." };
|
|
227
|
+
}
|
|
228
|
+
const ids = (body.data ?? []).map((m) => m.id).filter((id) => typeof id === "string");
|
|
229
|
+
if (ids.length === 0) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
fatal: true,
|
|
233
|
+
message: `${serverUrl} publishes an empty model menu, so gateway posture has nothing to serve.\n` +
|
|
234
|
+
"The gateway needs substitute routes — check FEST_ROUTES on the server.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, ids };
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Running it
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
const note = (s) => void process.stderr.write(`fest claude: ${s}\n`);
|
|
243
|
+
/**
|
|
244
|
+
* Warnings about the *chosen* posture. Collected separately from the notice
|
|
245
|
+
* line because the posture is now something Fest asserts rather than something
|
|
246
|
+
* the developer typed — so every consequence of that assertion is said out loud.
|
|
247
|
+
*/
|
|
248
|
+
export function postureWarnings(posture, env, triggers) {
|
|
249
|
+
const warnings = [];
|
|
250
|
+
const discovery = env["CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY"];
|
|
251
|
+
if (posture === "subscription") {
|
|
252
|
+
for (const trigger of triggers) {
|
|
253
|
+
warnings.push(`${trigger} will demote this session off your subscription. Clear it or expect org billing.`);
|
|
254
|
+
}
|
|
255
|
+
if (discovery !== undefined && discovery !== "") {
|
|
256
|
+
warnings.push("Gateway model discovery is unavailable in subscription posture. Use `fest claude gw`.");
|
|
257
|
+
}
|
|
258
|
+
return warnings;
|
|
259
|
+
}
|
|
260
|
+
if (discovery === undefined || discovery === "") {
|
|
261
|
+
warnings.push("Export CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1 to see gateway models in /model.");
|
|
262
|
+
}
|
|
263
|
+
if (env["_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL"]) {
|
|
264
|
+
// An explicit override: warn, don't strip.
|
|
265
|
+
warnings.push("_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL disables gateway discovery. Unset it.");
|
|
266
|
+
}
|
|
267
|
+
return warnings;
|
|
268
|
+
}
|
|
269
|
+
export async function runClaude(argv, requested = "auto") {
|
|
270
|
+
const cfg = await readCliConfig();
|
|
271
|
+
if (cfg === null) {
|
|
272
|
+
throw new Error("not logged in. Run `fest login` first.");
|
|
273
|
+
}
|
|
274
|
+
const login = requested === "auto" ? detectSubscriptionLogin() : null;
|
|
275
|
+
const posture = requested === "auto" ? (login.ok ? "subscription" : "key") : requested;
|
|
276
|
+
// Who pays, every time — not only on `gw`. Auto-detection changes the answer
|
|
277
|
+
// for a developer with no subscription login, so it must never be implicit.
|
|
278
|
+
if (requested !== "auto") {
|
|
279
|
+
note("gateway posture forced — org credentials pay.");
|
|
280
|
+
}
|
|
281
|
+
else if (posture === "subscription") {
|
|
282
|
+
note(`subscription posture (${login.detail}) — your own plan pays.`);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
note(`gateway posture (${login.detail}) — org credentials pay.`);
|
|
286
|
+
}
|
|
287
|
+
for (const w of postureWarnings(posture, process.env, posture === "subscription" ? settingsDemotionTriggers() : [])) {
|
|
288
|
+
note(w);
|
|
289
|
+
}
|
|
290
|
+
if (posture === "key") {
|
|
291
|
+
const menu = await fetchServableModels(cfg.serverUrl);
|
|
292
|
+
if (!menu.ok) {
|
|
293
|
+
if (menu.fatal)
|
|
294
|
+
throw new Error(menu.message);
|
|
295
|
+
note(menu.message);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const verdict = checkServable(menu.ids, resolveIntendedModel(argv, process.env));
|
|
299
|
+
if (verdict.kind === "fatal")
|
|
300
|
+
throw new Error(verdict.message);
|
|
301
|
+
if (verdict.kind === "warn")
|
|
302
|
+
note(verdict.message);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const env = buildClaudeEnv(process.env, cfg, posture);
|
|
306
|
+
const child = spawn("claude", argv, { stdio: "inherit", env });
|
|
307
|
+
const code = await new Promise((resolve, reject) => {
|
|
308
|
+
child.on("error", (err) => {
|
|
309
|
+
if (err.code === "ENOENT") {
|
|
310
|
+
reject(new Error("`claude` was not found on PATH. Install Claude Code first."));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
reject(err);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
child.on("exit", (exitCode, signal) => {
|
|
317
|
+
if (signal !== null) {
|
|
318
|
+
process.kill(process.pid, signal);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
resolve(exitCode ?? 1);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
process.exitCode = code;
|
|
325
|
+
}
|
|
326
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../cli/claude.ts"],"names":[],"mappings":"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"}
|