@workweave/router 0.1.2 → 0.1.4
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 +28 -10
- package/cc-statusline.sh +22 -12
- package/install.sh +527 -64
- package/package.json +1 -1
- package/uninstall.sh +112 -6
package/README.md
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# @workweave/router
|
|
2
2
|
|
|
3
|
-
One command, anywhere, to point Claude Code at the Weave Router.
|
|
3
|
+
One command, anywhere, to point Claude Code or Codex at the Weave Router.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx @workweave/router #
|
|
7
|
-
npx @workweave/router --
|
|
6
|
+
npx @workweave/router # interactive: pick Claude Code or Codex, then scope
|
|
7
|
+
npx @workweave/router --claude # skip the picker, target Claude Code
|
|
8
|
+
npx @workweave/router --codex # skip the picker, target the OpenAI Codex CLI
|
|
9
|
+
npx @workweave/router --scope project # per-repo install, commit settings.json (or .codex/)
|
|
8
10
|
npx @workweave/router --local # self-hosted via docker-compose (localhost:8080)
|
|
9
11
|
npx @workweave/router --base-url https://router.acme.internal
|
|
10
|
-
npx @workweave/router --non-interactive # reads $WEAVE_ROUTER_KEY, no prompts
|
|
12
|
+
npx @workweave/router --non-interactive # reads $WEAVE_ROUTER_KEY, no prompts (defaults to claude)
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
Version-pin for reproducible setups:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
npx @workweave/router@0.1.0 --scope project
|
|
18
|
+
npx @workweave/router@0.1.0 --claude --scope project
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
Uninstall:
|
|
20
22
|
|
|
21
23
|
```bash
|
|
22
|
-
npx @workweave/router --uninstall
|
|
23
|
-
npx @workweave/router --uninstall --
|
|
24
|
+
npx @workweave/router --uninstall # Claude Code, user scope
|
|
25
|
+
npx @workweave/router --uninstall --codex # Codex, user scope
|
|
26
|
+
npx @workweave/router --uninstall --scope project # Claude Code, inside the repo
|
|
27
|
+
npx @workweave/router --uninstall --codex --scope project
|
|
24
28
|
```
|
|
25
29
|
|
|
26
30
|
## What it does
|
|
@@ -28,8 +32,21 @@ npx @workweave/router --uninstall --scope project # in the repo
|
|
|
28
32
|
This package is a thin Node wrapper around [`install.sh`](./install.sh) from
|
|
29
33
|
the Weave Router repo. It exists so you can install from any machine with
|
|
30
34
|
Node ≥ 18 — no `curl | sh`, no Git clone, no PATH fiddling. Everything the
|
|
31
|
-
shell installer documents (scopes, flags, environment variables)
|
|
32
|
-
identically here.
|
|
35
|
+
shell installer documents (targets, scopes, flags, environment variables)
|
|
36
|
+
works identically here.
|
|
37
|
+
|
|
38
|
+
Two install targets:
|
|
39
|
+
|
|
40
|
+
- **Claude Code** (default) — patches `~/.claude/settings.json` (or
|
|
41
|
+
`<repo>/.claude/settings.json` with `--scope project`) so `claude` routes
|
|
42
|
+
through Weave automatically. Anthropic plan credentials flow through to
|
|
43
|
+
api.anthropic.com.
|
|
44
|
+
- **Codex** (`--codex`) — patches `~/.codex/config.toml` (or
|
|
45
|
+
`<repo>/.codex/config.toml`) with a managed `[model_providers.weave]`
|
|
46
|
+
block plus `model_provider = "weave"`. OpenAI plan credentials flow
|
|
47
|
+
through to api.openai.com. The block lives between begin/end markers so
|
|
48
|
+
re-running the installer rewrites it cleanly and `--uninstall --codex`
|
|
49
|
+
removes it without touching the rest of your config.
|
|
33
50
|
|
|
34
51
|
See the [main installer docs](https://github.com/workweave/router/tree/main/install)
|
|
35
52
|
for the full reference.
|
|
@@ -38,7 +55,8 @@ for the full reference.
|
|
|
38
55
|
|
|
39
56
|
- Node ≥ 18 (ships with `npx`)
|
|
40
57
|
- `bash` on PATH (macOS / Linux native; Windows needs Git Bash or WSL)
|
|
41
|
-
- `jq` on PATH — used by the status line script
|
|
58
|
+
- `jq` on PATH — used by the Claude Code status line script. Not required
|
|
59
|
+
for the Codex path.
|
|
42
60
|
|
|
43
61
|
## Why npx
|
|
44
62
|
|
package/cc-statusline.sh
CHANGED
|
@@ -133,8 +133,9 @@ prices='{
|
|
|
133
133
|
"claude-opus-4-6": 0.015,
|
|
134
134
|
"claude-opus-4-7": 0.015,
|
|
135
135
|
"claude-sonnet-4-5": 0.003,
|
|
136
|
+
"claude-sonnet-4-6": 0.003,
|
|
136
137
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
137
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
138
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
138
139
|
"gemini-2.0-flash": 0.0001,
|
|
139
140
|
"gemini-2.0-flash-lite": 0.000075,
|
|
140
141
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -161,20 +162,25 @@ prices='{
|
|
|
161
162
|
"gpt-5.5-mini": 0.0005,
|
|
162
163
|
"gpt-5.5-nano": 0.00015,
|
|
163
164
|
"gpt-5.5-pro": 0.03,
|
|
164
|
-
"
|
|
165
|
+
"minimax/minimax-m2.7": 0.0003,
|
|
166
|
+
"moonshotai/kimi-k2.5": 0.0006,
|
|
167
|
+
"moonshotai/kimi-k2.6": 0.00095,
|
|
165
168
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
166
|
-
"qwen/qwen3-
|
|
167
|
-
"qwen/qwen3-
|
|
168
|
-
"qwen/qwen3-
|
|
169
|
-
"
|
|
169
|
+
"qwen/qwen3-coder-next": 0.0005,
|
|
170
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
171
|
+
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
172
|
+
"xiaomi/mimo-v2.5": 0.0004,
|
|
173
|
+
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
174
|
+
"z-ai/glm-5": 0.0006
|
|
170
175
|
},
|
|
171
176
|
"output": {
|
|
172
177
|
"claude-haiku-4-5": 0.004,
|
|
173
178
|
"claude-opus-4-6": 0.075,
|
|
174
179
|
"claude-opus-4-7": 0.075,
|
|
175
180
|
"claude-sonnet-4-5": 0.015,
|
|
181
|
+
"claude-sonnet-4-6": 0.015,
|
|
176
182
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
177
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
183
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
178
184
|
"gemini-2.0-flash": 0.0004,
|
|
179
185
|
"gemini-2.0-flash-lite": 0.0003,
|
|
180
186
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -201,12 +207,16 @@ prices='{
|
|
|
201
207
|
"gpt-5.5-mini": 0.0025,
|
|
202
208
|
"gpt-5.5-nano": 0.0006,
|
|
203
209
|
"gpt-5.5-pro": 0.12,
|
|
204
|
-
"
|
|
210
|
+
"minimax/minimax-m2.7": 0.0012,
|
|
211
|
+
"moonshotai/kimi-k2.5": 0.003,
|
|
212
|
+
"moonshotai/kimi-k2.6": 0.004,
|
|
205
213
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
206
|
-
"qwen/qwen3-
|
|
207
|
-
"qwen/qwen3-
|
|
208
|
-
"qwen/qwen3-
|
|
209
|
-
"
|
|
214
|
+
"qwen/qwen3-coder-next": 0.0012,
|
|
215
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
216
|
+
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
217
|
+
"xiaomi/mimo-v2.5": 0.002,
|
|
218
|
+
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
219
|
+
"z-ai/glm-5": 0.00208
|
|
210
220
|
}
|
|
211
221
|
}'
|
|
212
222
|
# END_GENERATED_PRICES
|
package/install.sh
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
# writing the router base URL, router auth header, and a status line into
|
|
5
|
-
# Claude Code's settings.json. After running, `claude` Just Works — no shell
|
|
6
|
-
# exports, no manual settings edits.
|
|
3
|
+
# Weave Router installer for Claude Code and Codex.
|
|
7
4
|
#
|
|
8
|
-
#
|
|
5
|
+
# Configures Claude Code (default) or the OpenAI Codex CLI (`--codex`) to
|
|
6
|
+
# permanently route through the Weave Router. For Claude Code this writes
|
|
7
|
+
# the router base URL, router auth header, and a status line into Claude
|
|
8
|
+
# Code's settings.json. For Codex it writes a `model_providers.weave`
|
|
9
|
+
# entry plus `model_provider = "weave"` into ~/.codex/config.toml (managed
|
|
10
|
+
# block delimited by markers so re-install / uninstall is clean).
|
|
11
|
+
#
|
|
12
|
+
# Two scopes (apply to both targets):
|
|
9
13
|
# - user (default): ~/.claude/settings.json + ~/.weave/cc-statusline.sh
|
|
14
|
+
# ~/.codex/config.toml (with --codex)
|
|
10
15
|
# - project: <repo>/.claude/settings.json + <repo>/.claude/cc-statusline.sh
|
|
16
|
+
# <repo>/.codex/config.toml (with --codex)
|
|
11
17
|
#
|
|
12
18
|
# Or pass --dir to install into any directory:
|
|
13
19
|
# - dir: <dir>/.claude/settings.json + <dir>/.claude/cc-statusline.sh
|
|
20
|
+
# <dir>/.codex/config.toml (with --codex)
|
|
14
21
|
#
|
|
15
22
|
# Usage:
|
|
16
|
-
# npx @workweave/router #
|
|
23
|
+
# npx @workweave/router # interactive picker (Claude Code or Codex)
|
|
24
|
+
# npx @workweave/router --claude # skip the picker, target Claude Code
|
|
25
|
+
# npx @workweave/router --codex # skip the picker, target the OpenAI Codex CLI
|
|
17
26
|
# npx @workweave/router --scope project # commit-with-team install
|
|
18
27
|
# npx @workweave/router --dir /tmp/my-sandbox # isolated throwaway install
|
|
19
28
|
# npx @workweave/router --local # local router on localhost:8080
|
|
20
29
|
# npx @workweave/router --base-url http://localhost:8080 # self-hosted, custom port
|
|
21
|
-
# npx @workweave/router --non-interactive # require WEAVE_ROUTER_KEY env var
|
|
30
|
+
# npx @workweave/router --non-interactive # require WEAVE_ROUTER_KEY env var (defaults target to claude)
|
|
22
31
|
# npx @workweave/router --quiet # suppress banner, ping check, and trailing tips
|
|
23
32
|
# npx @workweave/router --uninstall # remove a previous install (delegates to uninstall.sh)
|
|
24
33
|
|
|
@@ -37,6 +46,14 @@ base_url=""
|
|
|
37
46
|
non_interactive="false"
|
|
38
47
|
quiet="false"
|
|
39
48
|
router_key_header="X-Weave-Router-Key"
|
|
49
|
+
# Target tool whose config we patch. "claude" (default) writes Claude Code
|
|
50
|
+
# settings.json; "codex" writes ~/.codex/config.toml. Each target carries its
|
|
51
|
+
# own credential-passthrough story in the router: Claude Code's logged-in
|
|
52
|
+
# Anthropic key flows through unchanged, and Codex's `OPENAI_API_KEY` flows
|
|
53
|
+
# through via the same header path. target_explicit tracks whether --claude
|
|
54
|
+
# or --codex was passed so an interactive run can prompt for the choice.
|
|
55
|
+
target="claude"
|
|
56
|
+
target_explicit="false"
|
|
40
57
|
|
|
41
58
|
# ---------- helpers ----------
|
|
42
59
|
|
|
@@ -78,11 +95,16 @@ skip() { printf "%s⊙%s %s%s%s\n" "$C_DIM" "$C_RESET" "$C_DIM" "$*" "$C_RESET";
|
|
|
78
95
|
print_banner() {
|
|
79
96
|
[ "$quiet" = "true" ] && return 0
|
|
80
97
|
[ "$tty_out" = "true" ] || return 0
|
|
98
|
+
local target_label
|
|
99
|
+
case "$target" in
|
|
100
|
+
codex) target_label="Codex installer" ;;
|
|
101
|
+
*) target_label="Claude Code installer" ;;
|
|
102
|
+
esac
|
|
81
103
|
printf '\n'
|
|
82
104
|
printf '%s ╦ ╦╔═╗╔═╗╦ ╦╔═╗%s\n' "$C_BRAND" "$C_RESET"
|
|
83
105
|
printf '%s ║║║║╣ ╠═╣╚╗╔╝║╣ %s\n' "$C_BRAND" "$C_RESET"
|
|
84
106
|
printf '%s ╚╩╝╚═╝╩ ╩ ╚╝ ╚═╝%s\n' "$C_BRAND" "$C_RESET"
|
|
85
|
-
printf ' %sWeave Router ·
|
|
107
|
+
printf ' %sWeave Router · %s%s\n\n' "$C_DIM" "$target_label" "$C_RESET"
|
|
86
108
|
}
|
|
87
109
|
|
|
88
110
|
# ---------- spinner ----------
|
|
@@ -207,6 +229,267 @@ refuse_if_symlink() {
|
|
|
207
229
|
fi
|
|
208
230
|
}
|
|
209
231
|
|
|
232
|
+
# Markers that delimit the block this installer manages inside Codex's
|
|
233
|
+
# config.toml. Kept on disk verbatim so a re-install (or uninstall.sh
|
|
234
|
+
# --codex) can find and replace the block instead of duplicating it.
|
|
235
|
+
WEAVE_CODEX_BEGIN_MARKER="# >>> weave-router managed (do not edit between markers) >>>"
|
|
236
|
+
WEAVE_CODEX_END_MARKER="# <<< weave-router managed <<<"
|
|
237
|
+
|
|
238
|
+
# ---------- identity helpers ----------
|
|
239
|
+
#
|
|
240
|
+
# The router parses X-Weave-User-Email and X-Weave-User-Name on every protocol
|
|
241
|
+
# (Anthropic, OpenAI, Gemini) and persists them onto router.model_router_users
|
|
242
|
+
# so customers can attribute traffic to a person even when many people share
|
|
243
|
+
# one API key. Claude Code's metadata.user_id carries only account_uuid (no
|
|
244
|
+
# email), so without these headers the router only ever sees anonymous UUIDs.
|
|
245
|
+
|
|
246
|
+
# normalize_email mirrors the router's proxy.NormalizeEmail: trim, lowercase,
|
|
247
|
+
# enforce a single '@' with non-empty local + domain parts, and cap at 254
|
|
248
|
+
# chars (RFC 5321). Returns the cleaned address on stdout, or empty string if
|
|
249
|
+
# the input is missing or malformed. We validate locally so the installer
|
|
250
|
+
# never plants a header value the router would silently drop, and so a
|
|
251
|
+
# typo'd git config doesn't end up as a per-request identity claim.
|
|
252
|
+
normalize_email() {
|
|
253
|
+
local raw="${1:-}"
|
|
254
|
+
# Trim whitespace then lowercase. tr is POSIX; the [:upper:]/[:lower:] form
|
|
255
|
+
# works on both macOS (BSD) and Linux (GNU) without needing LANG tweaks.
|
|
256
|
+
local trimmed="${raw#"${raw%%[![:space:]]*}"}"
|
|
257
|
+
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
258
|
+
local lowered
|
|
259
|
+
lowered="$(printf '%s' "$trimmed" | tr '[:upper:]' '[:lower:]')"
|
|
260
|
+
if [ -z "$lowered" ] || [ "${#lowered}" -gt 254 ]; then
|
|
261
|
+
printf ''
|
|
262
|
+
return
|
|
263
|
+
fi
|
|
264
|
+
# Reject any interior whitespace or control character so the value can't
|
|
265
|
+
# smuggle a second header into the newline-delimited ANTHROPIC_CUSTOM_HEADERS
|
|
266
|
+
# var. A valid email has none, so this is shape-only — not a deliverability
|
|
267
|
+
# check.
|
|
268
|
+
if printf '%s' "$lowered" | LC_ALL=C grep -q '[[:space:][:cntrl:]]'; then
|
|
269
|
+
printf ''
|
|
270
|
+
return
|
|
271
|
+
fi
|
|
272
|
+
case "$lowered" in
|
|
273
|
+
*@*@*) printf ''; return ;;
|
|
274
|
+
@*|*@) printf ''; return ;;
|
|
275
|
+
*@*) printf '%s' "$lowered" ;;
|
|
276
|
+
*) printf '' ;;
|
|
277
|
+
esac
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# normalize_name trims whitespace, rejects empty/oversized, and strips control
|
|
281
|
+
# chars + the colon/CR/LF chars HTTP forbids in header values. Display names
|
|
282
|
+
# are free-form so we don't case-fold; we just keep the header well-formed.
|
|
283
|
+
normalize_name() {
|
|
284
|
+
local raw="${1:-}"
|
|
285
|
+
local trimmed="${raw#"${raw%%[![:space:]]*}"}"
|
|
286
|
+
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
287
|
+
# Drop CR/LF/colon (header smuggling) and other control chars. tr's -d
|
|
288
|
+
# with a character class is portable across BSD/GNU.
|
|
289
|
+
local cleaned
|
|
290
|
+
cleaned="$(printf '%s' "$trimmed" | tr -d '\r\n:' | tr -d '[:cntrl:]')"
|
|
291
|
+
if [ -z "$cleaned" ] || [ "${#cleaned}" -gt 128 ]; then
|
|
292
|
+
printf ''
|
|
293
|
+
return
|
|
294
|
+
fi
|
|
295
|
+
printf '%s' "$cleaned"
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# resolve_user_email picks the email to plant in router request headers so the
|
|
299
|
+
# router can attribute traffic to a person even on shared API keys. Priority:
|
|
300
|
+
# WEAVE_USER_EMAIL env override → git config user.email → interactive prompt
|
|
301
|
+
# (pre-filled with whatever we found). In --non-interactive mode we never
|
|
302
|
+
# prompt, so unset/invalid means we ship no header (router treats that as
|
|
303
|
+
# account_uuid-only, same as today). Echoes the validated email on stdout.
|
|
304
|
+
resolve_user_email() {
|
|
305
|
+
local candidate=""
|
|
306
|
+
if [ -n "${WEAVE_USER_EMAIL:-}" ]; then
|
|
307
|
+
candidate="$(normalize_email "$WEAVE_USER_EMAIL")"
|
|
308
|
+
if [ -z "$candidate" ]; then
|
|
309
|
+
warn "WEAVE_USER_EMAIL=\"$WEAVE_USER_EMAIL\" is not a valid email; ignoring."
|
|
310
|
+
fi
|
|
311
|
+
fi
|
|
312
|
+
if [ -z "$candidate" ]; then
|
|
313
|
+
local git_email
|
|
314
|
+
git_email="$(git config --global user.email 2>/dev/null || true)"
|
|
315
|
+
candidate="$(normalize_email "$git_email")"
|
|
316
|
+
fi
|
|
317
|
+
if [ "$non_interactive" = "true" ] || [ ! -r /dev/tty ]; then
|
|
318
|
+
printf '%s' "$candidate"
|
|
319
|
+
return
|
|
320
|
+
fi
|
|
321
|
+
# Interactive: confirm/edit. Empty input keeps the suggested default; a
|
|
322
|
+
# literal `-` lets the user opt out (ship no header). This stays out of
|
|
323
|
+
# --quiet runs because --quiet implies the caller doesn't want prompts;
|
|
324
|
+
# they can still use WEAVE_USER_EMAIL to provide one explicitly.
|
|
325
|
+
if [ "$quiet" = "true" ]; then
|
|
326
|
+
printf '%s' "$candidate"
|
|
327
|
+
return
|
|
328
|
+
fi
|
|
329
|
+
local prompt_default="$candidate"
|
|
330
|
+
local response=""
|
|
331
|
+
if [ -n "$prompt_default" ]; then
|
|
332
|
+
printf "%sIdentify router traffic as %s[%s]%s (Enter to accept, '-' to skip): " \
|
|
333
|
+
"$C_DIM" "$C_BOLD" "$prompt_default" "$C_RESET" >/dev/tty
|
|
334
|
+
else
|
|
335
|
+
printf "%sEmail to identify your router traffic (blank to skip): %s" \
|
|
336
|
+
"$C_DIM" "$C_RESET" >/dev/tty
|
|
337
|
+
fi
|
|
338
|
+
read -r response </dev/tty || response=""
|
|
339
|
+
case "$response" in
|
|
340
|
+
"") printf '%s' "$prompt_default" ;;
|
|
341
|
+
"-") printf '' ;;
|
|
342
|
+
*)
|
|
343
|
+
local cleaned
|
|
344
|
+
cleaned="$(normalize_email "$response")"
|
|
345
|
+
if [ -z "$cleaned" ]; then
|
|
346
|
+
warn "\"$response\" is not a valid email; skipping identity header."
|
|
347
|
+
fi
|
|
348
|
+
printf '%s' "$cleaned"
|
|
349
|
+
;;
|
|
350
|
+
esac
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# write_codex_config writes a managed [model_providers.weave] block to the
|
|
354
|
+
# Codex CLI's config.toml. Sets `model_provider = "weave"` at the top level so
|
|
355
|
+
# Codex picks the routed provider by default. Both lines live inside the
|
|
356
|
+
# managed-block markers so uninstall removes them cleanly. We strip any
|
|
357
|
+
# top-level `model_provider = ...` declaration OUTSIDE the markers before
|
|
358
|
+
# appending so the file doesn't end up with a duplicate key (TOML rejects
|
|
359
|
+
# that). Inline `model_provider` keys inside `[profiles.*]` sections stay
|
|
360
|
+
# untouched.
|
|
361
|
+
#
|
|
362
|
+
# Usage: write_codex_config <config_file_path> <base_url> <api_key> [user_email] [user_name]
|
|
363
|
+
write_codex_config() {
|
|
364
|
+
local config_file="$1"
|
|
365
|
+
local block_url="$2"
|
|
366
|
+
local block_key="$3"
|
|
367
|
+
local block_email="${4:-}"
|
|
368
|
+
local block_name="${5:-}"
|
|
369
|
+
|
|
370
|
+
# Escape `\` and `"` for TOML basic strings. Order matters: replace
|
|
371
|
+
# backslashes first so the quotes we add next aren't double-escaped. A
|
|
372
|
+
# display name like `John "J" Doe` would otherwise produce invalid TOML and
|
|
373
|
+
# Codex would silently fail to parse config.toml — the installer's success
|
|
374
|
+
# message wouldn't help diagnose. Router keys are alnum+`_` from the API so
|
|
375
|
+
# safe as-is, but we escape uniformly for defense-in-depth.
|
|
376
|
+
toml_escape() {
|
|
377
|
+
local s="${1//\\/\\\\}"
|
|
378
|
+
printf '%s' "${s//\"/\\\"}"
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
local esc_key esc_email esc_name esc_url
|
|
382
|
+
esc_key="$(toml_escape "$block_key")"
|
|
383
|
+
esc_email="$(toml_escape "$block_email")"
|
|
384
|
+
esc_name="$(toml_escape "$block_name")"
|
|
385
|
+
esc_url="$(toml_escape "$block_url")"
|
|
386
|
+
|
|
387
|
+
# Plant whichever identity values we have alongside the router key so the
|
|
388
|
+
# router can attribute Codex traffic to a person on shared keys. Build the
|
|
389
|
+
# entries piecewise so an empty email/name is omitted entirely — the router
|
|
390
|
+
# never sees a header with no value (and TOML rejects empty unquoted vals).
|
|
391
|
+
local headers_parts="\"X-Weave-Router-Key\" = \"${esc_key}\""
|
|
392
|
+
if [ -n "$block_email" ]; then
|
|
393
|
+
headers_parts="${headers_parts}, \"X-Weave-User-Email\" = \"${esc_email}\""
|
|
394
|
+
fi
|
|
395
|
+
if [ -n "$block_name" ]; then
|
|
396
|
+
headers_parts="${headers_parts}, \"X-Weave-User-Name\" = \"${esc_name}\""
|
|
397
|
+
fi
|
|
398
|
+
local headers_line="http_headers = { ${headers_parts} }"
|
|
399
|
+
|
|
400
|
+
local block
|
|
401
|
+
block="$(cat <<TOML
|
|
402
|
+
${WEAVE_CODEX_BEGIN_MARKER}
|
|
403
|
+
# Managed by the Weave Router installer. Re-running the installer rewrites
|
|
404
|
+
# this block; \`./uninstall.sh --codex\` removes it. To opt out without
|
|
405
|
+
# uninstalling, change the model_provider value below.
|
|
406
|
+
model_provider = "weave"
|
|
407
|
+
|
|
408
|
+
[model_providers.weave]
|
|
409
|
+
name = "Weave Router"
|
|
410
|
+
base_url = "${esc_url}/v1"
|
|
411
|
+
wire_api = "responses"
|
|
412
|
+
${headers_line}
|
|
413
|
+
${WEAVE_CODEX_END_MARKER}
|
|
414
|
+
TOML
|
|
415
|
+
)"
|
|
416
|
+
|
|
417
|
+
if [ -f "$config_file" ]; then
|
|
418
|
+
local tmp; tmp="$(mktemp -t weave-codex.XXXXXX)"
|
|
419
|
+
# Strip the managed block (between markers) plus any top-level
|
|
420
|
+
# `model_provider =` outside it. We define "top-level" as everything
|
|
421
|
+
# before the first `[section]` header. The awk handles both passes in
|
|
422
|
+
# one sweep so we never emit a duplicate.
|
|
423
|
+
awk -v begin="$WEAVE_CODEX_BEGIN_MARKER" -v end="$WEAVE_CODEX_END_MARKER" '
|
|
424
|
+
$0 == begin { skip = 1; next }
|
|
425
|
+
$0 == end { skip = 0; next }
|
|
426
|
+
skip { next }
|
|
427
|
+
/^[[:space:]]*\[/ { in_section = 1 }
|
|
428
|
+
!in_section && /^[[:space:]]*model_provider[[:space:]]*=/ { next }
|
|
429
|
+
{ print }
|
|
430
|
+
' "$config_file" >"$tmp"
|
|
431
|
+
|
|
432
|
+
# Insert the managed block at TOML top-level scope, NOT end-of-file. In
|
|
433
|
+
# TOML, every bare key after a `[section]` header belongs to that
|
|
434
|
+
# section, so appending `model_provider = "weave"` after a user's
|
|
435
|
+
# existing `[profiles.foo]` would silently scope it as
|
|
436
|
+
# `profiles.foo.model_provider` — Codex would never see the top-level
|
|
437
|
+
# default and routing would silently fail to activate. We splice the
|
|
438
|
+
# block in just before the first user section header so:
|
|
439
|
+
# <user's top-level keys> ← still top-level
|
|
440
|
+
# <our managed block> ← model_provider stays top-level
|
|
441
|
+
# [model_providers.weave] ← scoped section, OK anywhere
|
|
442
|
+
# <user's sections> ← re-scope, unaffected
|
|
443
|
+
local first_section
|
|
444
|
+
first_section="$(awk '/^[[:space:]]*\[/ { print NR; exit }' "$tmp")"
|
|
445
|
+
if [ -n "$first_section" ]; then
|
|
446
|
+
# BSD `head -n 0` (macOS default) errors with "illegal line count"
|
|
447
|
+
# and trips `set -euo pipefail`, leaving an empty config. Skip the
|
|
448
|
+
# head call entirely when the file starts with a section header.
|
|
449
|
+
{
|
|
450
|
+
if [ "$first_section" -gt 1 ]; then
|
|
451
|
+
head -n "$((first_section - 1))" "$tmp"
|
|
452
|
+
fi
|
|
453
|
+
printf "%s\n" "$block"
|
|
454
|
+
tail -n "+${first_section}" "$tmp"
|
|
455
|
+
} >"$config_file"
|
|
456
|
+
else
|
|
457
|
+
# No section headers in the existing file — every prior user key was
|
|
458
|
+
# already at top-level. Our block ends with its own [section], so
|
|
459
|
+
# appending is safe (no bare keys follow).
|
|
460
|
+
cp "$tmp" "$config_file"
|
|
461
|
+
printf "\n%s\n" "$block" >>"$config_file"
|
|
462
|
+
fi
|
|
463
|
+
rm -f "$tmp"
|
|
464
|
+
else
|
|
465
|
+
printf "%s\n" "$block" >"$config_file"
|
|
466
|
+
fi
|
|
467
|
+
# 0600: the file holds a router key. Even at user scope, mode 644 would
|
|
468
|
+
# leak the key to any local user on a shared box.
|
|
469
|
+
chmod 600 "$config_file"
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
# resolve_user_name mirrors resolve_user_email but for display name. Priority:
|
|
473
|
+
# WEAVE_USER_NAME env override → git config user.name → empty. We don't
|
|
474
|
+
# prompt for name independently: if email prompting yielded nothing, name
|
|
475
|
+
# almost certainly will too, and a second prompt is noise. Echoes the
|
|
476
|
+
# validated name on stdout.
|
|
477
|
+
resolve_user_name() {
|
|
478
|
+
local candidate=""
|
|
479
|
+
if [ -n "${WEAVE_USER_NAME:-}" ]; then
|
|
480
|
+
candidate="$(normalize_name "$WEAVE_USER_NAME")"
|
|
481
|
+
if [ -z "$candidate" ]; then
|
|
482
|
+
warn "WEAVE_USER_NAME=\"$WEAVE_USER_NAME\" is not a usable name; ignoring."
|
|
483
|
+
fi
|
|
484
|
+
fi
|
|
485
|
+
if [ -z "$candidate" ]; then
|
|
486
|
+
local git_name
|
|
487
|
+
git_name="$(git config --global user.name 2>/dev/null || true)"
|
|
488
|
+
candidate="$(normalize_name "$git_name")"
|
|
489
|
+
fi
|
|
490
|
+
printf '%s' "$candidate"
|
|
491
|
+
}
|
|
492
|
+
|
|
210
493
|
# ---------- uninstall delegation ----------
|
|
211
494
|
#
|
|
212
495
|
# `--uninstall` flips this script into a thin shim for uninstall.sh: the
|
|
@@ -285,6 +568,14 @@ while [ $# -gt 0 ]; do
|
|
|
285
568
|
install_dir="${2:-}"; shift 2
|
|
286
569
|
[ -n "$install_dir" ] || { err "--dir requires a path"; exit 2; }
|
|
287
570
|
;;
|
|
571
|
+
--codex)
|
|
572
|
+
target="codex"; target_explicit="true"; shift
|
|
573
|
+
;;
|
|
574
|
+
--claude)
|
|
575
|
+
# No-op selector for symmetry with --codex. Useful in pipelines that
|
|
576
|
+
# want to skip the interactive picker without depending on the default.
|
|
577
|
+
target="claude"; target_explicit="true"; shift
|
|
578
|
+
;;
|
|
288
579
|
-h|--help)
|
|
289
580
|
usage 0
|
|
290
581
|
;;
|
|
@@ -300,9 +591,31 @@ fi
|
|
|
300
591
|
# trim trailing slash for cleanliness
|
|
301
592
|
base_url="${base_url%/}"
|
|
302
593
|
|
|
594
|
+
# ---------- interactive target prompt ----------
|
|
595
|
+
|
|
596
|
+
# If neither --claude nor --codex was passed and we have a controlling
|
|
597
|
+
# terminal, ask which tool to install for. Non-interactive runs (CI,
|
|
598
|
+
# `curl | sh --non-interactive`) silently use the "claude" default — same
|
|
599
|
+
# behavior the script had before --codex existed, so existing pipelines
|
|
600
|
+
# don't change semantics. We prompt BEFORE print_banner so the banner's
|
|
601
|
+
# target label (Claude Code installer / Codex installer) reflects the choice.
|
|
602
|
+
if [ "$target_explicit" = "false" ] && [ "$non_interactive" = "false" ] && [ -r /dev/tty ]; then
|
|
603
|
+
printf "%sInstall target:%s\n" "$C_BOLD" "$C_RESET"
|
|
604
|
+
printf " %s1)%s Claude Code %s— patches ~/.claude/settings.json (or <repo>/.claude/)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
605
|
+
printf " %s2)%s Codex %s— patches ~/.codex/config.toml (or <repo>/.codex/)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
606
|
+
printf "Choose %s[1/2]%s (default %s1%s): " "$C_BOLD" "$C_RESET" "$C_BOLD" "$C_RESET"
|
|
607
|
+
read -r target_choice </dev/tty || target_choice=""
|
|
608
|
+
case "${target_choice:-1}" in
|
|
609
|
+
1|""|claude|c|C) target="claude" ;;
|
|
610
|
+
2|codex|x|X) target="codex" ;;
|
|
611
|
+
*) err "invalid choice: $target_choice"; exit 2 ;;
|
|
612
|
+
esac
|
|
613
|
+
fi
|
|
614
|
+
|
|
303
615
|
# Banner runs before the interactive scope prompt so the very first thing
|
|
304
616
|
# users see when `make full-setup` hands off to install.sh is the wordmark,
|
|
305
|
-
# not a bare "Install scope:" line.
|
|
617
|
+
# not a bare "Install scope:" line. Target prompt above already finalized
|
|
618
|
+
# $target, so the banner's per-target label reflects the user's choice.
|
|
306
619
|
print_banner
|
|
307
620
|
|
|
308
621
|
# ---------- interactive scope prompt ----------
|
|
@@ -311,9 +624,22 @@ print_banner
|
|
|
311
624
|
# scope to install into. Non-interactive runs (CI, `curl | sh --non-interactive`)
|
|
312
625
|
# silently use the "user" default.
|
|
313
626
|
if [ -z "$install_dir" ] && [ "$scope_explicit" = "false" ] && [ "$non_interactive" = "false" ] && [ -r /dev/tty ]; then
|
|
627
|
+
# Per-target paths so the prompt text matches what actually gets written.
|
|
628
|
+
case "$target" in
|
|
629
|
+
codex)
|
|
630
|
+
scope_user_path="~/.codex/"
|
|
631
|
+
scope_project_path="<repo>/.codex/"
|
|
632
|
+
scope_cli_label="codex"
|
|
633
|
+
;;
|
|
634
|
+
*)
|
|
635
|
+
scope_user_path="~/.claude/"
|
|
636
|
+
scope_project_path="<repo>/.claude/"
|
|
637
|
+
scope_cli_label="claude"
|
|
638
|
+
;;
|
|
639
|
+
esac
|
|
314
640
|
printf "%sInstall scope:%s\n" "$C_BOLD" "$C_RESET"
|
|
315
|
-
printf " %s1)%s user %s— write to
|
|
316
|
-
printf " %s2)%s project %s— write to
|
|
641
|
+
printf " %s1)%s user %s— write to %s (applies everywhere you run %s)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$scope_user_path" "$scope_cli_label" "$C_RESET"
|
|
642
|
+
printf " %s2)%s project %s— write to %s (applies only inside this repo)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$scope_project_path" "$C_RESET"
|
|
317
643
|
printf "Choose %s[1/2]%s (default %s1%s): " "$C_BOLD" "$C_RESET" "$C_BOLD" "$C_RESET"
|
|
318
644
|
read -r scope_choice </dev/tty || scope_choice=""
|
|
319
645
|
case "${scope_choice:-1}" in
|
|
@@ -345,15 +671,29 @@ fi
|
|
|
345
671
|
|
|
346
672
|
# ---------- pre-flight ----------
|
|
347
673
|
|
|
348
|
-
[ "$quiet" = "true" ] || info "scope=${C_BOLD}${scope}${C_RESET} base_url=${C_BOLD}${base_url}${C_RESET}"
|
|
674
|
+
[ "$quiet" = "true" ] || info "scope=${C_BOLD}${scope}${C_RESET} target=${C_BOLD}${target}${C_RESET} base_url=${C_BOLD}${base_url}${C_RESET}"
|
|
349
675
|
|
|
350
|
-
|
|
676
|
+
# Codex install only writes a TOML file (managed via awk) so jq isn't needed.
|
|
677
|
+
# Claude Code's settings.json patching uses jq to deep-merge env keys.
|
|
678
|
+
if [ "$target" = "claude" ]; then
|
|
679
|
+
require_cmd jq "macOS: 'brew install jq' · Debian/Ubuntu: 'sudo apt install jq'"
|
|
680
|
+
fi
|
|
351
681
|
require_cmd curl "macOS/Linux: usually preinstalled — check your package manager"
|
|
352
682
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
683
|
+
case "$target" in
|
|
684
|
+
claude)
|
|
685
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
686
|
+
warn "'claude' not found on PATH. Install Claude Code from https://claude.com/code, then re-run this script."
|
|
687
|
+
warn "Continuing — settings.json will be written and will take effect once Claude Code is installed."
|
|
688
|
+
fi
|
|
689
|
+
;;
|
|
690
|
+
codex)
|
|
691
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
692
|
+
warn "'codex' not found on PATH. Install via 'npm install -g @openai/codex' (or brew install codex), then re-run this script."
|
|
693
|
+
warn "Continuing — config.toml will be written and will take effect once Codex is installed."
|
|
694
|
+
fi
|
|
695
|
+
;;
|
|
696
|
+
esac
|
|
357
697
|
|
|
358
698
|
script_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
359
699
|
|
|
@@ -386,42 +726,59 @@ else
|
|
|
386
726
|
esac
|
|
387
727
|
fi
|
|
388
728
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
;;
|
|
398
|
-
project)
|
|
399
|
-
settings_dir="$settings_base/.claude"
|
|
400
|
-
settings_file="$settings_dir/settings.json"
|
|
401
|
-
local_settings_file="$settings_dir/settings.local.json"
|
|
402
|
-
statusline_dir="$settings_base/.claude"
|
|
403
|
-
statusline_file="$statusline_dir/cc-statusline.sh"
|
|
404
|
-
# Portable relative path for real repos (teammates can clone anywhere).
|
|
405
|
-
# Absolute path when --dir overrides (no meaningful $CLAUDE_PROJECT_DIR).
|
|
406
|
-
if [ -z "$install_dir" ]; then
|
|
407
|
-
statusline_path_for_settings="\${CLAUDE_PROJECT_DIR}/.claude/cc-statusline.sh"
|
|
408
|
-
else
|
|
729
|
+
if [ "$target" = "claude" ]; then
|
|
730
|
+
case "$scope" in
|
|
731
|
+
user)
|
|
732
|
+
settings_dir="$settings_base/.claude"
|
|
733
|
+
settings_file="$settings_dir/settings.json"
|
|
734
|
+
local_settings_file=""
|
|
735
|
+
statusline_dir="${settings_base}/.weave"
|
|
736
|
+
statusline_file="$statusline_dir/cc-statusline.sh"
|
|
409
737
|
statusline_path_for_settings="$statusline_file"
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
738
|
+
;;
|
|
739
|
+
project)
|
|
740
|
+
settings_dir="$settings_base/.claude"
|
|
741
|
+
settings_file="$settings_dir/settings.json"
|
|
742
|
+
local_settings_file="$settings_dir/settings.local.json"
|
|
743
|
+
statusline_dir="$settings_base/.claude"
|
|
744
|
+
statusline_file="$statusline_dir/cc-statusline.sh"
|
|
745
|
+
# Portable relative path for real repos (teammates can clone anywhere).
|
|
746
|
+
# Absolute path when --dir overrides (no meaningful $CLAUDE_PROJECT_DIR).
|
|
747
|
+
if [ -z "$install_dir" ]; then
|
|
748
|
+
statusline_path_for_settings="\${CLAUDE_PROJECT_DIR}/.claude/cc-statusline.sh"
|
|
749
|
+
else
|
|
750
|
+
statusline_path_for_settings="$statusline_file"
|
|
751
|
+
fi
|
|
752
|
+
;;
|
|
753
|
+
esac
|
|
413
754
|
|
|
414
|
-
# Symlink containment: refuse if any target path is a symlink. User-scope
|
|
415
|
-
# under $HOME are trusted; project-scope and --dir paths come from a
|
|
416
|
-
# user-supplied directory that may be hostile, so we check those.
|
|
417
|
-
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
fi
|
|
755
|
+
# Symlink containment: refuse if any target path is a symlink. User-scope
|
|
756
|
+
# paths under $HOME are trusted; project-scope and --dir paths come from a
|
|
757
|
+
# git repo or user-supplied directory that may be hostile, so we check those.
|
|
758
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
759
|
+
refuse_if_symlink "$settings_dir"
|
|
760
|
+
refuse_if_symlink "$settings_file"
|
|
761
|
+
refuse_if_symlink "$local_settings_file"
|
|
762
|
+
refuse_if_symlink "$statusline_file"
|
|
763
|
+
fi
|
|
423
764
|
|
|
424
|
-
mkdir -p "$settings_dir" "$statusline_dir"
|
|
765
|
+
mkdir -p "$settings_dir" "$statusline_dir"
|
|
766
|
+
else
|
|
767
|
+
# Codex CLI reads config from ~/.codex/config.toml by default. For project
|
|
768
|
+
# scope we write to <repo>/.codex/config.toml; the user invokes Codex with
|
|
769
|
+
# `CODEX_HOME=<repo>/.codex codex` (or runs from the repo if Codex auto-
|
|
770
|
+
# discovers). The router key is embedded in the file so it stays per-
|
|
771
|
+
# teammate — .codex/config.toml goes in .gitignore in project scope.
|
|
772
|
+
codex_dir="$settings_base/.codex"
|
|
773
|
+
codex_config_file="$codex_dir/config.toml"
|
|
774
|
+
|
|
775
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
776
|
+
refuse_if_symlink "$codex_dir"
|
|
777
|
+
refuse_if_symlink "$codex_config_file"
|
|
778
|
+
fi
|
|
779
|
+
|
|
780
|
+
mkdir -p "$codex_dir"
|
|
781
|
+
fi
|
|
425
782
|
|
|
426
783
|
# ---------- token handling ----------
|
|
427
784
|
|
|
@@ -453,6 +810,89 @@ if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
|
453
810
|
[ -n "$api_key" ] || { err "no key provided"; exit 1; }
|
|
454
811
|
fi
|
|
455
812
|
|
|
813
|
+
# ---------- identity (user email + name) ----------
|
|
814
|
+
#
|
|
815
|
+
# The router parses X-Weave-User-Email and X-Weave-User-Name on every protocol
|
|
816
|
+
# (Anthropic, OpenAI/Codex, Gemini) and persists them onto
|
|
817
|
+
# router.model_router_users so customers can attribute traffic to a person even
|
|
818
|
+
# when many people share one API key. We plant the headers at install time
|
|
819
|
+
# because Claude Code's metadata.user_id payload carries only account_uuid (no
|
|
820
|
+
# email), and Codex carries no identity at all — without this step the router
|
|
821
|
+
# only ever sees anonymous UUIDs for non-OTLP customers.
|
|
822
|
+
#
|
|
823
|
+
# Gate name on email: when the user explicitly opts out of email identity (via
|
|
824
|
+
# '-' at the prompt or by clearing git config), don't auto-plant a name from
|
|
825
|
+
# git config either. Opt-out should be all-or-nothing so the router
|
|
826
|
+
# consistently sees zero identity headers when the user wants to stay
|
|
827
|
+
# anonymous.
|
|
828
|
+
user_email="$(resolve_user_email)"
|
|
829
|
+
if [ -n "$user_email" ]; then
|
|
830
|
+
user_name="$(resolve_user_name)"
|
|
831
|
+
else
|
|
832
|
+
user_name=""
|
|
833
|
+
fi
|
|
834
|
+
if [ -n "$user_email" ] && [ -n "$user_name" ]; then
|
|
835
|
+
ok "Will identify router traffic as $user_name <$user_email>"
|
|
836
|
+
elif [ -n "$user_email" ]; then
|
|
837
|
+
ok "Will identify router traffic as $user_email"
|
|
838
|
+
else
|
|
839
|
+
info "No identity set — router traffic will be attributed by account UUID only."
|
|
840
|
+
fi
|
|
841
|
+
|
|
842
|
+
# ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
|
|
843
|
+
|
|
844
|
+
if [ "$target" = "codex" ]; then
|
|
845
|
+
write_codex_config "$codex_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
846
|
+
ok "Codex config written to $codex_config_file"
|
|
847
|
+
|
|
848
|
+
# Project scope: ensure the per-teammate config (which holds the router key)
|
|
849
|
+
# is gitignored. The base URL is the same for every teammate, so a
|
|
850
|
+
# committed shared file would still leak the per-key portion. Easier to
|
|
851
|
+
# ignore the whole config and have each teammate run the installer.
|
|
852
|
+
if [ "$scope" = "project" ] && [ -z "$install_dir" ] && [ -n "${git_root:-}" ]; then
|
|
853
|
+
gitignore="$git_root/.gitignore"
|
|
854
|
+
refuse_if_symlink "$gitignore"
|
|
855
|
+
for entry in \
|
|
856
|
+
".codex/config.toml"
|
|
857
|
+
do
|
|
858
|
+
if [ ! -f "$gitignore" ] || ! grep -qxF "$entry" "$gitignore"; then
|
|
859
|
+
printf '%s\n' "$entry" >>"$gitignore"
|
|
860
|
+
fi
|
|
861
|
+
done
|
|
862
|
+
ok "Updated $gitignore (ignored .codex/config.toml)"
|
|
863
|
+
fi
|
|
864
|
+
|
|
865
|
+
# Post-install verification: same probes the Claude path runs so a working
|
|
866
|
+
# install gives the same green checks regardless of target.
|
|
867
|
+
if [ "$quiet" != "true" ]; then
|
|
868
|
+
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
869
|
+
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
870
|
+
fi
|
|
871
|
+
fi
|
|
872
|
+
|
|
873
|
+
if [ -n "$api_key" ]; then
|
|
874
|
+
# Pass the router key via stdin (`@-`) instead of -H so it never lands in
|
|
875
|
+
# the process arg list. Mirrors the Claude-path validate logic.
|
|
876
|
+
validate_codex_key() {
|
|
877
|
+
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
878
|
+
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
879
|
+
}
|
|
880
|
+
if ! spin "Validating API key" validate_codex_key; then
|
|
881
|
+
warn "Router rejected the API key (check it matches the dashboard at $base_url/ui/)."
|
|
882
|
+
fi
|
|
883
|
+
fi
|
|
884
|
+
|
|
885
|
+
printf "\n"
|
|
886
|
+
printf "%s✓%s %s%sWeave Router installed for Codex.%s\n" \
|
|
887
|
+
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
888
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
889
|
+
# Codex auto-discovers ~/.codex; for non-user installs the caller has to
|
|
890
|
+
# point CODEX_HOME at the directory we wrote so Codex finds our config.
|
|
891
|
+
info "Run Codex with CODEX_HOME=$codex_dir codex so it picks up this config."
|
|
892
|
+
fi
|
|
893
|
+
exit 0
|
|
894
|
+
fi
|
|
895
|
+
|
|
456
896
|
# ---------- write the statusline script ----------
|
|
457
897
|
|
|
458
898
|
cat > "$statusline_file" << 'STATUSLINE_EOF'
|
|
@@ -591,8 +1031,9 @@ prices='{
|
|
|
591
1031
|
"claude-opus-4-6": 0.015,
|
|
592
1032
|
"claude-opus-4-7": 0.015,
|
|
593
1033
|
"claude-sonnet-4-5": 0.003,
|
|
1034
|
+
"claude-sonnet-4-6": 0.003,
|
|
594
1035
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
595
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
1036
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
596
1037
|
"gemini-2.0-flash": 0.0001,
|
|
597
1038
|
"gemini-2.0-flash-lite": 0.000075,
|
|
598
1039
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -619,20 +1060,25 @@ prices='{
|
|
|
619
1060
|
"gpt-5.5-mini": 0.0005,
|
|
620
1061
|
"gpt-5.5-nano": 0.00015,
|
|
621
1062
|
"gpt-5.5-pro": 0.03,
|
|
622
|
-
"
|
|
1063
|
+
"minimax/minimax-m2.7": 0.0003,
|
|
1064
|
+
"moonshotai/kimi-k2.5": 0.0006,
|
|
1065
|
+
"moonshotai/kimi-k2.6": 0.00095,
|
|
623
1066
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
624
|
-
"qwen/qwen3-
|
|
625
|
-
"qwen/qwen3-
|
|
626
|
-
"qwen/qwen3-
|
|
627
|
-
"
|
|
1067
|
+
"qwen/qwen3-coder-next": 0.0005,
|
|
1068
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
1069
|
+
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
1070
|
+
"xiaomi/mimo-v2.5": 0.0004,
|
|
1071
|
+
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
1072
|
+
"z-ai/glm-5": 0.0006
|
|
628
1073
|
},
|
|
629
1074
|
"output": {
|
|
630
1075
|
"claude-haiku-4-5": 0.004,
|
|
631
1076
|
"claude-opus-4-6": 0.075,
|
|
632
1077
|
"claude-opus-4-7": 0.075,
|
|
633
1078
|
"claude-sonnet-4-5": 0.015,
|
|
1079
|
+
"claude-sonnet-4-6": 0.015,
|
|
634
1080
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
635
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
1081
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
636
1082
|
"gemini-2.0-flash": 0.0004,
|
|
637
1083
|
"gemini-2.0-flash-lite": 0.0003,
|
|
638
1084
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -659,12 +1105,16 @@ prices='{
|
|
|
659
1105
|
"gpt-5.5-mini": 0.0025,
|
|
660
1106
|
"gpt-5.5-nano": 0.0006,
|
|
661
1107
|
"gpt-5.5-pro": 0.12,
|
|
662
|
-
"
|
|
1108
|
+
"minimax/minimax-m2.7": 0.0012,
|
|
1109
|
+
"moonshotai/kimi-k2.5": 0.003,
|
|
1110
|
+
"moonshotai/kimi-k2.6": 0.004,
|
|
663
1111
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
664
|
-
"qwen/qwen3-
|
|
665
|
-
"qwen/qwen3-
|
|
666
|
-
"qwen/qwen3-
|
|
667
|
-
"
|
|
1112
|
+
"qwen/qwen3-coder-next": 0.0012,
|
|
1113
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
1114
|
+
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
1115
|
+
"xiaomi/mimo-v2.5": 0.002,
|
|
1116
|
+
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
1117
|
+
"z-ai/glm-5": 0.00208
|
|
668
1118
|
}
|
|
669
1119
|
}'
|
|
670
1120
|
# END_GENERATED_PRICES
|
|
@@ -835,13 +1285,26 @@ tmp_patch="$(mktemp)"
|
|
|
835
1285
|
# leave the cursor hidden if Ctrl-C lands during settings.json patching.
|
|
836
1286
|
trap '_spin_cleanup; rm -f "$tmp_patch"' EXIT INT TERM HUP
|
|
837
1287
|
|
|
1288
|
+
# Claude Code splits ANTHROPIC_CUSTOM_HEADERS on newlines, so multiple headers
|
|
1289
|
+
# ride in the same env var separated by \n. Append identity headers alongside
|
|
1290
|
+
# the router key so a single var carries them all. When email/name are empty
|
|
1291
|
+
# we keep the bare router-key form so a re-install for a user who opted out
|
|
1292
|
+
# cleanly removes the old line.
|
|
1293
|
+
custom_headers="$router_key_header: $api_key"
|
|
1294
|
+
if [ -n "$user_email" ]; then
|
|
1295
|
+
custom_headers="$custom_headers"$'\n'"X-Weave-User-Email: $user_email"
|
|
1296
|
+
fi
|
|
1297
|
+
if [ -n "$user_name" ]; then
|
|
1298
|
+
custom_headers="$custom_headers"$'\n'"X-Weave-User-Name: $user_name"
|
|
1299
|
+
fi
|
|
1300
|
+
|
|
838
1301
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
839
1302
|
jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
|
|
840
1303
|
env: { ANTHROPIC_BASE_URL: $url },
|
|
841
1304
|
statusLine: { type: "command", command: $sl }
|
|
842
1305
|
}' >"$tmp_patch"
|
|
843
1306
|
else
|
|
844
|
-
jq -n --arg url "$base_url" --arg header "$
|
|
1307
|
+
jq -n --arg url "$base_url" --arg header "$custom_headers" --arg sl "$statusline_path_for_settings" '{
|
|
845
1308
|
env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header },
|
|
846
1309
|
statusLine: { type: "command", command: $sl }
|
|
847
1310
|
}' >"$tmp_patch"
|
|
@@ -867,7 +1330,7 @@ fi
|
|
|
867
1330
|
ok "Settings written to $settings_file"
|
|
868
1331
|
|
|
869
1332
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
870
|
-
jq -n --arg header "$
|
|
1333
|
+
jq -n --arg header "$custom_headers" '{
|
|
871
1334
|
env: { ANTHROPIC_CUSTOM_HEADERS: $header }
|
|
872
1335
|
}' >"$tmp_patch"
|
|
873
1336
|
if [ -f "$local_settings_file" ]; then
|
package/package.json
CHANGED
package/uninstall.sh
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# Weave Router uninstaller for Claude Code.
|
|
3
|
+
# Weave Router uninstaller for Claude Code and Codex.
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# Default target is Claude Code: removes the env vars, statusLine, and local
|
|
6
|
+
# router auth that install.sh added; leaves the rest of settings.json
|
|
7
|
+
# untouched. Pass --codex to strip the managed [model_providers.weave]
|
|
8
|
+
# block (and the matching top-level `model_provider`) from Codex's
|
|
9
|
+
# config.toml — anything outside the markers is preserved.
|
|
7
10
|
#
|
|
8
11
|
# Usage:
|
|
9
|
-
# npx @workweave/router --uninstall # user scope
|
|
12
|
+
# npx @workweave/router --uninstall # Claude Code, user scope
|
|
13
|
+
# npx @workweave/router --uninstall --codex # Codex, user scope
|
|
10
14
|
# npx @workweave/router --uninstall --scope project # run inside the repo
|
|
11
15
|
# npx @workweave/router --uninstall --dir /tmp/test # --dir alone (user scope, .weave/)
|
|
12
16
|
# npx @workweave/router --uninstall --scope project --dir /tmp # --dir + project scope (.claude/)
|
|
@@ -16,6 +20,7 @@ set -euo pipefail
|
|
|
16
20
|
scope="user"
|
|
17
21
|
scope_explicit="false"
|
|
18
22
|
install_dir=""
|
|
23
|
+
target="claude"
|
|
19
24
|
|
|
20
25
|
err() { printf "\033[31merror:\033[0m %s\n" "$*" >&2; }
|
|
21
26
|
info() { printf "\033[36m==>\033[0m %s\n" "$*"; }
|
|
@@ -44,6 +49,15 @@ while [ $# -gt 0 ]; do
|
|
|
44
49
|
install_dir="${2:-}"; shift 2
|
|
45
50
|
[ -n "$install_dir" ] || { err "--dir requires a path"; exit 2; }
|
|
46
51
|
;;
|
|
52
|
+
--codex)
|
|
53
|
+
target="codex"; shift
|
|
54
|
+
;;
|
|
55
|
+
--claude)
|
|
56
|
+
# No-op selector for symmetry with --codex and install.sh's --claude.
|
|
57
|
+
# Lets `./install.sh --uninstall --claude` (which forwards remaining
|
|
58
|
+
# args here) succeed instead of hitting the unknown-flag catch-all.
|
|
59
|
+
target="claude"; shift
|
|
60
|
+
;;
|
|
47
61
|
-h|--help)
|
|
48
62
|
# awk avoids GNU `head -n -<N>` (rejected by BSD head on macOS).
|
|
49
63
|
awk 'NR<2 { next } /^set -euo/ { exit } { sub(/^# ?/, ""); print }' "$0"
|
|
@@ -55,11 +69,103 @@ while [ $# -gt 0 ]; do
|
|
|
55
69
|
esac
|
|
56
70
|
done
|
|
57
71
|
|
|
58
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
59
|
-
err "jq is required."
|
|
72
|
+
if [ "$target" = "claude" ] && ! command -v jq >/dev/null 2>&1; then
|
|
73
|
+
err "jq is required for the Claude Code uninstall path."
|
|
60
74
|
exit 1
|
|
61
75
|
fi
|
|
62
76
|
|
|
77
|
+
# Markers must stay in sync with install.sh. Keep verbatim.
|
|
78
|
+
WEAVE_CODEX_BEGIN_MARKER="# >>> weave-router managed (do not edit between markers) >>>"
|
|
79
|
+
WEAVE_CODEX_END_MARKER="# <<< weave-router managed <<<"
|
|
80
|
+
|
|
81
|
+
# strip_codex_block rewrites config.toml without the managed block and any
|
|
82
|
+
# top-level `model_provider = "weave"` that lived outside the markers (which
|
|
83
|
+
# can happen if the user copy-pasted our key into their own config). Other
|
|
84
|
+
# top-level model_provider values are preserved so we don't yank a user back
|
|
85
|
+
# into the OpenAI default when they meant to keep their own.
|
|
86
|
+
strip_codex_block() {
|
|
87
|
+
local config_file="$1"
|
|
88
|
+
local tmp; tmp="$(mktemp -t weave-codex-uninstall.XXXXXX)"
|
|
89
|
+
awk -v begin="$WEAVE_CODEX_BEGIN_MARKER" -v end="$WEAVE_CODEX_END_MARKER" '
|
|
90
|
+
$0 == begin { skip = 1; next }
|
|
91
|
+
$0 == end { skip = 0; next }
|
|
92
|
+
skip { next }
|
|
93
|
+
/^[[:space:]]*\[/ { in_section = 1 }
|
|
94
|
+
!in_section && /^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"[[:space:]]*$/ { next }
|
|
95
|
+
{ print }
|
|
96
|
+
' "$config_file" >"$tmp"
|
|
97
|
+
mv "$tmp" "$config_file"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ---------- codex uninstall path ----------
|
|
101
|
+
|
|
102
|
+
if [ "$target" = "codex" ]; then
|
|
103
|
+
# Resolve codex_config_file based on scope/dir. Mirrors the install path so
|
|
104
|
+
# an `install --codex --scope project` is exactly reversed by an
|
|
105
|
+
# `uninstall --codex --scope project`.
|
|
106
|
+
if [ -n "$install_dir" ]; then
|
|
107
|
+
install_dir="$(cd "$install_dir" 2>/dev/null && pwd || echo "$install_dir")"
|
|
108
|
+
codex_dir="$install_dir/.codex"
|
|
109
|
+
refuse_if_symlink "$codex_dir"
|
|
110
|
+
elif [ "$scope" = "user" ]; then
|
|
111
|
+
codex_dir="$HOME/.codex"
|
|
112
|
+
else
|
|
113
|
+
# Project scope: same prompt + git-root fallback as install.sh.
|
|
114
|
+
project_dir=""
|
|
115
|
+
if [ "$scope_explicit" = "false" ] && [ -r /dev/tty ]; then
|
|
116
|
+
default_project_dir="$(pwd)"
|
|
117
|
+
printf "Project directory to uninstall from [default: %s]: " "$default_project_dir"
|
|
118
|
+
read -r project_dir_choice </dev/tty || project_dir_choice=""
|
|
119
|
+
project_dir="${project_dir_choice:-$default_project_dir}"
|
|
120
|
+
case "$project_dir" in
|
|
121
|
+
"~") project_dir="$HOME" ;;
|
|
122
|
+
"~/"*) project_dir="$HOME/${project_dir#~/}" ;;
|
|
123
|
+
esac
|
|
124
|
+
if [ ! -d "$project_dir" ]; then
|
|
125
|
+
err "directory does not exist: $project_dir"
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
project_dir="$(cd "$project_dir" && pwd)"
|
|
129
|
+
fi
|
|
130
|
+
if [ -n "${project_dir:-}" ]; then
|
|
131
|
+
codex_dir="$project_dir/.codex"
|
|
132
|
+
else
|
|
133
|
+
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
|
134
|
+
err "--scope project must be run inside a git repo, or use --dir <path>."
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
codex_dir="$git_root/.codex"
|
|
138
|
+
fi
|
|
139
|
+
refuse_if_symlink "$codex_dir"
|
|
140
|
+
fi
|
|
141
|
+
codex_config_file="$codex_dir/config.toml"
|
|
142
|
+
refuse_if_symlink "$codex_config_file"
|
|
143
|
+
|
|
144
|
+
if [ -f "$codex_config_file" ]; then
|
|
145
|
+
strip_codex_block "$codex_config_file"
|
|
146
|
+
# If the file now contains only whitespace/comments, leave it: the user
|
|
147
|
+
# may have other comments worth keeping. Truly empty files we delete so
|
|
148
|
+
# we don't leave a zero-byte artifact behind.
|
|
149
|
+
if [ ! -s "$codex_config_file" ]; then
|
|
150
|
+
rm -f "$codex_config_file"
|
|
151
|
+
ok "Removed empty $codex_config_file"
|
|
152
|
+
else
|
|
153
|
+
ok "Cleaned $codex_config_file"
|
|
154
|
+
fi
|
|
155
|
+
else
|
|
156
|
+
info "No Codex config at $codex_config_file (already uninstalled?)"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
if [ -n "$install_dir" ]; then
|
|
160
|
+
ok "Weave Router uninstalled from $install_dir (Codex)."
|
|
161
|
+
else
|
|
162
|
+
ok "Weave Router uninstalled (Codex, scope=$scope)."
|
|
163
|
+
fi
|
|
164
|
+
exit 0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# ---------- claude uninstall path ----------
|
|
168
|
+
|
|
63
169
|
# Resolve the base directory. When --dir is given, use it directly.
|
|
64
170
|
if [ -n "$install_dir" ]; then
|
|
65
171
|
install_dir="$(cd "$install_dir" 2>/dev/null && pwd || echo "$install_dir")"
|