proxitor 0.9.0-beta.0 โ†’ 0.9.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,90 +1,70 @@
1
1
  # proxitor
2
2
 
3
3
  <p align="center">
4
- <strong>A transparent proxy between your AI CLI tools and OpenRouter.</strong><br/>
5
- Route by provider. Control costs. Keep streaming. Zero config changes in Claude Code.
4
+ <strong>A friendly proxy between your AI CLI tools and OpenRouter.</strong><br/>
5
+ Route requests to the provider you want. Keep prompt caching alive. Cut costs.<br/>
6
+ Your tools don't even notice.
6
7
  </p>
7
8
 
8
9
  <p align="center">
9
10
  <a href="https://www.npmjs.com/package/proxitor"><img src="https://img.shields.io/npm/v/proxitor?color=6366f1&labelColor=1e2327&label=npm" alt="npm version"></a>
10
- <a href="https://github.com/neiromaster/proxitor/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-22c55e?labelColor=1e2327" alt="MIT License"></a>
11
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-22c55e?labelColor=1e2327" alt="MIT License"></a>
11
12
  <img src="https://img.shields.io/badge/node-%3E%3D22-3b82f6?labelColor=1e2327" alt="Node.js โ‰ฅ 22">
12
- <img src="https://img.shields.io/badge/built_with-TypeScript-3178c6?labelColor=1e2327" alt="TypeScript">
13
13
  </p>
14
14
 
15
- ---
15
+ ๐ŸŒ **English** ยท [ะ ัƒััะบะธะน](./docs/README.ru.md)
16
16
 
17
- ```
18
- Claude Code / Codex
19
- โ”‚
20
- โ”‚ ANTHROPIC_BASE_URL=http://localhost:8828/v1
21
- โ–ผ
22
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
23
- โ”‚ proxitor โ”‚ โ† injects provider routing
24
- โ”‚ :8828 โ”‚ โ† streams SSE back unchanged
25
- โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
26
- โ”‚
27
- โ”‚ + X-OpenRouter-* headers
28
- โ–ผ
29
- OpenRouter
30
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”
31
- Anthropic DeepInfra Azure ...
32
- ```
17
+ <p align="center"><img src="./docs/assets/proxitor-wizard.gif" alt="proxitor setup wizard" width="640"></p>
33
18
 
34
19
  ---
35
20
 
36
- ## Why
21
+ Proxitor sits between Claude Code (or Codex, or any Anthropic/OpenAI-compatible CLI) and [OpenRouter](https://openrouter.ai). One API key, every model โ€” but **you** decide which provider serves each request, and you make prompt caching actually work.
37
22
 
38
- ### The prompt cache problem
23
+ ```
24
+ your AI CLI โ†’ proxitor โ†’ OpenRouter โ†’ the provider you picked
25
+ ```
39
26
 
40
- OpenRouter is convenient โ€” one key, every model. But by default it load-balances across multiple provider instances for the same model. Each request can land on a different provider, and **prompt caching is provider-scoped**: a cache entry built on Anthropic's infrastructure doesn't help when the next request goes to DeepInfra.
27
+ ## Why you'd want this
41
28
 
42
- Claude Code sends a large system prompt on every single request. Without a pinned provider, you pay full token price every time. With proxitor locking `claude-*` to `anthropic`, that system prompt gets cached after the first hit and subsequent requests cost a fraction.
29
+ OpenRouter is convenient โ€” one key, every model. But it load-balances across providers, and **prompt caching is provider-scoped**: a cache built on Anthropic doesn't help when the next request lands on DeepInfra. Claude Code sends a big system prompt on every request, so without a pinned provider you pay full price every time.
43
30
 
44
- ```yaml
45
- # pin all Claude models to Anthropic โ€” prompt cache works reliably
46
- modelOverrides:
47
- "claude-*":
48
- provider:
49
- only: "anthropic"
50
- ```
31
+ Pin `claude-*` to `anthropic`, and that system prompt gets cached after the first hit. Subsequent requests cost a fraction.
51
32
 
52
- ### Other reasons to use it
33
+ A few other things it's good for:
53
34
 
54
- - **Cost control** โ€” route specific models to cheaper providers when caching isn't the priority
55
- - **Automatic fallbacks** โ€” if Anthropic is degraded, fall back to DeepInfra without touching your tools
56
- - **Mixed routing** โ€” `claude-*` on Anthropic, `gpt-*` on Azure, different rules per model
57
- - **Data privacy** โ€” enforce `dataCollection: deny` or ZDR across all requests
35
+ - **Cost control** โ€” route specific models to cheaper providers when caching isn't the priority.
36
+ - **Automatic fallbacks** โ€” Anthropic down? Fall back to DeepInfra without touching your tools.
37
+ - **Mixed routing** โ€” `claude-*` on Anthropic, `gpt-*` on Azure, different rules per model.
38
+ - **Privacy** โ€” enforce `dataCollection: deny` or zero-data-retention across everything.
58
39
 
59
- Proxitor sits between your CLI tools and OpenRouter, injecting all of this transparently. Your tools don't know anything changed.
60
-
61
- ---
40
+ > Proxitor injects all of this transparently. Your tools see a normal API. Nothing on their side changes.
62
41
 
63
42
  ## Install
64
43
 
44
+ Requires **Node.js 22+**.
45
+
65
46
  ```sh
66
- # npm
67
47
  npm install -g proxitor
68
-
69
- # bun
70
- bun install -g proxitor
71
-
72
- # no install needed
73
- npx proxitor
48
+ # or: bun install -g proxitor
49
+ # or run it once, no install: npx proxitor
74
50
  ```
75
51
 
76
- ---
52
+ ## Quick start
77
53
 
78
- ## Quick Start
54
+ **1. Set it up** โ€” the wizard asks a few questions and writes your config:
79
55
 
80
- **1. Start the proxy**
56
+ ```sh
57
+ proxitor config wizard
58
+ ```
59
+
60
+ **2. Run it**
81
61
 
82
62
  ```sh
83
- OPENROUTER_API_KEY=sk-or-... proxitor
63
+ proxitor
84
64
  # Listening on http://0.0.0.0:8828
85
65
  ```
86
66
 
87
- **2. Point your tools at it**
67
+ **3. Point your tool at it**
88
68
 
89
69
  ```sh
90
70
  # Claude Code
@@ -94,350 +74,59 @@ ANTHROPIC_BASE_URL=http://localhost:8828/v1 claude
94
74
  OPENAI_BASE_URL=http://localhost:8828/v1 codex
95
75
  ```
96
76
 
97
- That's it. Requests flow through proxitor to OpenRouter, SSE streams pass through unchanged.
98
-
99
- ---
100
-
101
- ## Configuration
102
-
103
- Proxitor looks for a config file in this order:
104
-
105
- ```
106
- proxitor.config.yaml โ†’ proxitor.config.yml โ†’ proxitor.config.json
107
- .proxitor.yaml โ†’ .proxitor.yml โ†’ .proxitor.json
108
- ```
109
-
110
- **Priority:** CLI flags > config file > environment variables > defaults
111
-
112
- All defaults are derived from a single Zod schema (`DEFAULTS`) โ€” no hardcoded constants scattered across modules. Config values are validated through Zod on load, including the final merged result.
113
-
114
- See [`proxitor.config.example.yaml`](./proxitor.config.example.yaml) for the complete reference.
115
-
116
- ### Authentication type
117
-
118
- By default, proxitor sends the API key as a `Bearer` token (`Authorization: Bearer sk-...`). If you're using a custom proxy provider that expects an `OAuth` header instead, set `authType` to `oauth`:
119
-
120
- ```yaml
121
- authType: oauth # "bearer" (default) or "oauth"
122
- ```
123
-
124
- This changes the header to `Authorization: OAuth sk-...`.
125
-
126
- ### Custom API URL and data fallback
127
-
128
- When using a custom `openrouterBaseUrl` that points to a third-party service, that service may not support OpenRouter-specific endpoints like `/providers` or `/models/{author}/{slug}/endpoints`. Proxitor handles this automatically:
129
-
130
- - **Automatic fallback** โ€” if the custom API returns an error (4xx/5xx) or an unexpected response format for data endpoints, proxitor falls back to `https://openrouter.ai/api/v1` (no API key needed โ€” these endpoints are public)
131
- - **`openrouterDataUrl`** โ€” set this explicitly to control the primary URL for data fetching, independent of `openrouterBaseUrl` (which is used for proxying requests)
132
-
133
- ```yaml
134
- # Proxy requests go to custom service, data fetching falls back to OpenRouter
135
- openrouterBaseUrl: 'https://custom-service.example.com/v1'
136
-
137
- # Explicitly set the primary data URL (optional, defaults to openrouterBaseUrl)
138
- # openrouterDataUrl: 'https://openrouter.ai/api/v1'
139
- ```
140
-
141
- When a fallback occurs, proxitor logs a warning: `Custom API did not return providers, using OpenRouter data as fallback`.
142
-
143
- ### Provider routing
144
-
145
- Control which provider handles your requests. All three options accept a string or an array:
146
-
147
- ```yaml
148
- # Strict lock โ€” only this provider, no fallbacks
149
- provider:
150
- only: "anthropic"
151
-
152
- # Restricted pool โ€” load balance between these providers only
153
- provider:
154
- only:
155
- - "anthropic"
156
- - "deepinfra"
157
-
158
- # Priority order โ€” try Anthropic first, fall back to others if unavailable
159
- provider:
160
- order: "anthropic"
161
- allowFallbacks: true
162
-
163
- # Strict order โ€” try in sequence, no fallbacks outside the list
164
- provider:
165
- order:
166
- - "anthropic"
167
- - "deepinfra"
168
- allowFallbacks: false
169
-
170
- # Blacklist โ€” never use these providers
171
- provider:
172
- ignore: "azure"
173
- ```
174
-
175
- | Option | Behavior |
176
- |---|---|
177
- | `only` | Restrict to the listed provider(s). Load balances by price within the list. Never routes outside it โ€” if all are unavailable, the request fails. |
178
- | `order` | Try providers in the specified priority order. If none work, falls back to other available providers (unless `allowFallbacks: false`). |
179
- | `ignore` | Never route to the listed provider(s). |
180
-
181
- Without `provider` set, requests are forwarded unchanged.
182
-
183
- See [OpenRouter's provider routing docs](https://openrouter.ai/docs/guides/routing/provider-selection) for the full list of supported providers and options.
184
-
185
- ### Per-model overrides
186
-
187
- Route different models differently. Keys are exact names or prefix wildcards. More specific matches win.
188
-
189
- ```yaml
190
- provider:
191
- order: "deepinfra" # global default
192
-
193
- modelOverrides:
194
- # Exact match โ€” force this model to Anthropic
195
- "claude-sonnet-4-6":
196
- provider:
197
- only: "anthropic"
198
-
199
- # Wildcard โ€” all claude-* models prefer Anthropic with fallback
200
- "claude-*":
201
- provider:
202
- order:
203
- - "anthropic"
204
- - "deepinfra"
205
-
206
- # GPT models to OpenAI/Azure, plus a custom header
207
- "gpt-*":
208
- provider:
209
- only:
210
- - "openai"
211
- - "azure"
212
- headers:
213
- X-Model-Family: "gpt"
214
- ```
215
-
216
- **Match priority:** exact name > longer prefix > shorter prefix.
77
+ That's the whole setup. Requests flow through proxitor; streaming responses pass through untouched.
217
78
 
218
- ### Custom headers
79
+ ## Configuring it
219
80
 
220
- Add headers to all proxied requests, or per-model (merged on top of global):
221
-
222
- ```yaml
223
- headers:
224
- X-Custom-Header: "my-value"
225
- X-Environment: "production"
226
-
227
- modelOverrides:
228
- "claude-*":
229
- headers:
230
- X-Custom-Header: "claude-override" # overrides the global value
231
- X-Extra: "only-for-claude" # added only for this model
232
- ```
233
-
234
- ### Advanced provider options
235
-
236
- ```yaml
237
- provider:
238
- sort: "throughput" # sort by: price | throughput | latency
239
- quantizations:
240
- - "fp8" # filter by quantization level
241
- maxPrice:
242
- prompt: 1 # $/M tokens
243
- completion: 2
244
- requireParameters: true # only use providers that support all request params
245
- dataCollection: "deny" # "allow" | "deny"
246
- zdr: true # Zero Data Retention enforcement
247
- preferredMinThroughput:
248
- p90: 50 # tokens/sec (soft threshold)
249
- preferredMaxLatency:
250
- p90: 3 # seconds (soft threshold)
251
- ```
252
-
253
- ### Prompt caching
254
-
255
- By default, OpenRouter doesn't enable prompt caching โ€” every request pays full token price. Proxitor can inject `cache_control` and `session_id` to make caching work automatically.
256
-
257
- **`cacheControl`** โ€” injects `cache_control: { "type": "ephemeral" }` into the request body. OpenRouter uses this to set cache breakpoints and advance them as conversations grow.
258
-
259
- **`sessionId`** โ€” injects `session_id` for provider sticky routing. Without it, OpenRouter only pins to a provider after detecting a cache hit. With it, routing sticks from the **first request** โ€” critical for OpenAI models where delayed caching means 0 cached tokens on the first 1-2 requests.
260
-
261
- Both support `auto` / `always` / `never` modes:
262
-
263
- | Mode | `cacheControl` | `sessionId` |
264
- |---|---|---|
265
- | `auto` (default) | Anthropic models on `/v1/chat/completions`; all models on `/v1/messages` and `/v1/responses` | Use `X-Claude-Code-Session-Id` header if present; otherwise generate proxy UUID |
266
- | `always` | All models, all endpoints | Generate a proxy UUID for sticky routing |
267
- | `never` | Disabled | Disabled |
268
-
269
- ```yaml
270
- cacheControl: auto # safe default โ€” Anthropic and safe endpoints only
271
- sessionId: auto # always ensures sticky routing (client header or proxy UUID)
272
-
273
- # Force caching for all models (may cause 400 on non-Anthropic /v1/chat/completions)
274
- # cacheControl: always
275
-
276
- # Per-model overrides
277
- modelOverrides:
278
- "gpt-*":
279
- cacheControl: never # OpenAI caches automatically, no injection needed
280
- sessionId: always # but sticky routing still helps
281
- ```
282
-
283
- **Why both matter:**
284
- - **Anthropic models** โ€” `cache_control` activates caching, `session_id` prevents provider flip-flopping that would invalidate it
285
- - **OpenAI models** โ€” caching is automatic (no `cache_control` needed), but `session_id` ensures sticky routing from request #1 instead of waiting for a cache hit
286
- - **All models** โ€” `session_id` prevents the provider switch that silently resets cache
287
-
288
- ### Health check
81
+ The friendly way: an interactive menu โ€” no YAML required.
289
82
 
290
83
  ```sh
291
- curl http://localhost:8828/health
292
- ```
293
-
294
- ### Cache usage logging
295
-
296
- Proxitor automatically logs cache token usage from upstream responses โ€” both non-streaming JSON and streaming SSE. No configuration needed.
297
-
298
- ```
299
- [abc123] Cache read: 50000, write: 25000 tokens
300
- [def456] Cache: no cached tokens
84
+ proxitor config # open the menu
85
+ proxitor config wizard # (re)run guided setup
301
86
  ```
302
87
 
303
- Supports both provider formats:
88
+ From the menu you can set your API key and connection, pick routing per model (with live provider pricing), tune caching, and add or edit model overrides. It pulls live data from OpenRouter, so you browse real models and providers with up-to-date prices.
304
89
 
305
- | Provider format | Fields |
306
- |---|---|
307
- | Anthropic | `usage.cache_read_input_tokens` / `usage.cache_creation_input_tokens` |
308
- | OpenAI / OpenRouter | `usage.prompt_tokens_details.cached_tokens` / `cache_write_tokens` |
309
-
310
- When both formats are present (e.g., OpenRouter relaying an Anthropic response), Anthropic fields take priority.
311
-
312
- ---
90
+ Prefer to edit a file? The full **[configuration reference](./docs/configuration.md)** covers provider routing, per-model overrides, headers, caching modes, and every option. [`proxitor.config.example.yaml`](./proxitor.config.example.yaml) is a commented template.
313
91
 
314
- ## Interactive Config Manager
92
+ ## Adding a model override
315
93
 
316
- Proxitor includes an interactive CLI for managing model overrides โ€” search models, pick providers, and write to config without editing YAML by hand.
94
+ Pin a model โ€” or a wildcard like `claude-*` โ€” to specific providers, straight from the menu. It pulls live pricing and latency for every provider of that model.
317
95
 
318
- ### Setup wizard
96
+ <p align="center"><img src="./docs/assets/proxitor-add.gif" alt="proxitor: add a model override" width="640"></p>
319
97
 
320
- Run the wizard to create or update your config interactively. If no config exists, any command will offer to launch it automatically.
98
+ ## When something's off
321
99
 
322
100
  ```sh
323
- proxitor config wizard
101
+ proxitor doctor # checks environment, config, key, network, port, version
324
102
  ```
325
103
 
326
- The wizard asks for:
327
-
328
- - **OpenRouter API key** โ€” stored in config or set as `OPENROUTER_API_KEY` env var
329
- - **Port** โ€” default `8828` (avoids conflicts with common dev servers on 8080)
330
- - **API base URL** โ€” default `https://openrouter.ai/api/v1`; change for self-hosted or custom endpoints
331
- - **Data URL** โ€” separate URL for provider/model data fetching; falls back to OpenRouter automatically if the custom API doesn't support these endpoints
332
- - **Authentication type** โ€” `bearer` (default) or `oauth`; use `oauth` for custom proxy providers that pass tokens in the `Authorization: OAuth ...` header
333
- - **Host** โ€” all interfaces (`0.0.0.0`) or localhost only (`127.0.0.1`)
334
- - **Save location** โ€” project directory, `~/.config/proxitor/`, or `$XDG_CONFIG_HOME/proxitor/`
335
-
336
- If a config already exists, the wizard shows its location and asks whether to reconfigure. Existing `modelOverrides`, `provider`, and other fields are preserved โ€” only the wizard fields are updated.
337
-
338
- ```sh
339
- proxitor config menu # interactive menu
340
- proxitor config add # add a model override
341
- proxitor config edit # edit existing override
342
- proxitor config remove # remove override(s)
343
- proxitor config list # show current overrides
344
- proxitor config browse # explore models with pricing info
345
- proxitor config wizard # interactive setup wizard
346
- proxitor config validate # validate config file
347
- ```
104
+ It prints a clear report and exits non-zero if anything fails โ€” handy from CI too (`--json`, `--offline`, `--timeout`).
348
105
 
349
- ### Add override walkthrough
106
+ While proxitor runs, it logs cache usage from upstream so you can see whether caching is actually helping:
350
107
 
351
- ```sh
352
- $ proxitor config add
353
-
354
- โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
355
- โ”‚ Add Model Override โ”‚
356
- โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
357
-
358
- โ—‡ Search for a model
359
- โ”‚ claude
360
- (23 matches)
361
- โ— anthropic/claude-sonnet-4-6 ยท $3.00/$15.00 ยท 200k
362
- โ—‹ anthropic/claude-opus-4-8 ยท $15.00/$75.00 ยท 200k
363
- ...
364
-
365
- โ—‡ Configure provider routing
366
- โ”‚ โ—‹ Use specific providers only
367
- โ—‹ Set provider priority order
368
- โ—‹ Ignore specific providers
369
- โ—‹ Skip provider routing
370
108
  ```
371
-
372
- **"Use specific providers only" / "Ignore specific providers"** โ€” multiselect, pick all that apply:
373
-
374
- ```text
375
- โ—‡ Select providers
376
- โ—ผ anthropic (anthropic) ยท 1.0s ยท 40 t/s
377
- โ—ป google-vertex/global ยท 1.1s ยท 39 t/s
378
- โ—ป amazon-bedrock ยท 1.2s ยท 40 t/s
109
+ [abc123] Cache read: 50000, write: 25000 tokens (99.6% hit)
379
110
  ```
380
111
 
381
- **"Set provider priority order"** โ€” pick providers one at a time, then select **โœ“ Done** at the bottom to finish:
382
-
383
- ```text
384
- โ—‡ Select provider #1 (or cancel to finish)
385
- โ”‚ โ— anthropic (anthropic) ยท 1.0s ยท 40 t/s
386
- โ—‹ google-vertex/global ยท 1.1s ยท 39 t/s
387
- โ—‹ amazon-bedrock ยท 1.2s ยท 40 t/s
388
- โ—‹ โœ“ Done
389
-
390
- โ—‡ Select provider #2 (or cancel to finish)
391
- โ”‚ โ— google-vertex/global ยท 1.1s ยท 39 t/s
392
- โ—‹ amazon-bedrock ยท 1.2s ยท 40 t/s
393
- โ—‹ โœ“ Done
394
-
395
- โ—‡ Select provider #3 (or cancel to finish)
396
- โ”‚ โ— โœ“ Done
112
+ Quick health poke: `curl http://localhost:8828/health`.
397
113
 
398
- โ—‡ Allow fallbacks to other providers? Yes
114
+ ## Commands at a glance
399
115
 
400
- โ—‡ Save to config? Yes
401
-
402
- โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
403
- โ”‚ โœ“ Model override saved โ”‚
404
- โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ
116
+ ```sh
117
+ proxitor # start the proxy (the default command)
118
+ proxitor config # interactive config menu
119
+ proxitor config wizard # guided setup
120
+ proxitor config browse # explore models + pricing
121
+ proxitor doctor # diagnose everything
122
+ proxitor --help # the rest of the flags
405
123
  ```
406
124
 
407
- The interface uses live data from the OpenRouter API โ€” model search with type-ahead, real provider availability and pricing for each model.
408
-
409
- ---
410
-
411
- ## CLI Options
412
-
413
- | Flag | Default | Description |
414
- |---|---|---|
415
- | `-p, --port <port>` | `8828` | Server port |
416
- | `-h, --host <host>` | `0.0.0.0` | Server host |
417
- | `-c, --config <path>` | auto-discovered | Path to config file |
418
- | `--openrouter-key <key>` | `$OPENROUTER_API_KEY` | OpenRouter API key |
419
- | `--verbose` | `false` | Enable verbose logging |
420
- | `--no-config` | | Skip config file discovery |
421
- | `-v, --version` | | Print version |
422
- | `--help` | | Print help |
125
+ Common flags: `--port`, `--host`, `--config <path>`, `--openrouter-key <key>`. Run `proxitor --help` and `proxitor config --help` for the full list.
423
126
 
424
- ---
425
-
426
- ## Development
427
-
428
- ```sh
429
- pnpm install # install dependencies
430
- pnpm dev # build + watch
431
- pnpm test # run tests
432
- pnpm test:e2e # end-to-end tests
433
- pnpm typecheck # TypeScript check
434
- pnpm check:biome # lint + format check
435
- pnpm lint:fix # auto-fix lint issues
436
- pnpm build # production build
437
- pnpm check # typecheck + biome + test (full CI)
438
- ```
127
+ ## Contributing
439
128
 
440
- ---
129
+ PRs welcome โ€” see **[CONTRIBUTING.md](./CONTRIBUTING.md)** for setup, tests, commits, and changesets.
441
130
 
442
131
  ## License
443
132