@workweave/router 0.1.6 → 0.1.7
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 +15 -0
- package/cc-statusline.sh +12 -10
- package/commands/router-off.md +10 -0
- package/commands/router-on.md +10 -0
- package/commands/router-status.md +10 -0
- package/install.sh +377 -17
- package/package.json +1 -1
- package/uninstall.sh +21 -2
package/README.md
CHANGED
|
@@ -19,6 +19,21 @@ Version-pin for reproducible setups:
|
|
|
19
19
|
npx @workweave/router@0.1.0 --claude --scope project
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Switch on/off without uninstalling (keeps your config so switching back is
|
|
23
|
+
instant; requires an explicit client):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @workweave/router off --claude # route Claude Code directly to Anthropic
|
|
27
|
+
npx @workweave/router on --claude # route Claude Code through the router again
|
|
28
|
+
npx @workweave/router status --codex # is Codex on the router or direct?
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Claude Code reads its router setting at launch, so quit and reopen it after an
|
|
32
|
+
on/off. Codex and opencode pick it up on their next run. Inside Claude Code the
|
|
33
|
+
slash commands `/router-off`, `/router-on`, and `/router-status` do the same.
|
|
34
|
+
Cursor has no config file we own — toggle its base URL override in **Settings →
|
|
35
|
+
Models** instead.
|
|
36
|
+
|
|
22
37
|
Uninstall:
|
|
23
38
|
|
|
24
39
|
```bash
|
package/cc-statusline.sh
CHANGED
|
@@ -130,8 +130,9 @@ normalize_model() {
|
|
|
130
130
|
prices='{
|
|
131
131
|
"input": {
|
|
132
132
|
"claude-haiku-4-5": 0.0008,
|
|
133
|
-
"claude-opus-4-6": 0.
|
|
134
|
-
"claude-opus-4-7": 0.
|
|
133
|
+
"claude-opus-4-6": 0.005,
|
|
134
|
+
"claude-opus-4-7": 0.005,
|
|
135
|
+
"claude-opus-4-8": 0.005,
|
|
135
136
|
"claude-sonnet-4-5": 0.003,
|
|
136
137
|
"claude-sonnet-4-6": 0.003,
|
|
137
138
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
@@ -167,21 +168,22 @@ prices='{
|
|
|
167
168
|
"mistralai/mistral-small-2603": 0.0002,
|
|
168
169
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
169
170
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
170
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
171
|
+
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
171
172
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
172
173
|
"qwen/qwen3-coder": 0.0009,
|
|
173
174
|
"qwen/qwen3-coder-next": 0.0005,
|
|
174
175
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
175
176
|
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
176
177
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
177
|
-
"xiaomi/mimo-v2.5": 0.0004,
|
|
178
178
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
179
|
-
"z-ai/glm-5": 0.0006
|
|
179
|
+
"z-ai/glm-5": 0.0006,
|
|
180
|
+
"z-ai/glm-5.1": 0.00105
|
|
180
181
|
},
|
|
181
182
|
"output": {
|
|
182
183
|
"claude-haiku-4-5": 0.004,
|
|
183
|
-
"claude-opus-4-6": 0.
|
|
184
|
-
"claude-opus-4-7": 0.
|
|
184
|
+
"claude-opus-4-6": 0.025,
|
|
185
|
+
"claude-opus-4-7": 0.025,
|
|
186
|
+
"claude-opus-4-8": 0.025,
|
|
185
187
|
"claude-sonnet-4-5": 0.015,
|
|
186
188
|
"claude-sonnet-4-6": 0.015,
|
|
187
189
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
@@ -217,16 +219,16 @@ prices='{
|
|
|
217
219
|
"mistralai/mistral-small-2603": 0.0006,
|
|
218
220
|
"moonshotai/kimi-k2.5": 0.003,
|
|
219
221
|
"moonshotai/kimi-k2.6": 0.004,
|
|
220
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
222
|
+
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
221
223
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
222
224
|
"qwen/qwen3-coder": 0.0027,
|
|
223
225
|
"qwen/qwen3-coder-next": 0.0012,
|
|
224
226
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
225
227
|
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
226
228
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
227
|
-
"xiaomi/mimo-v2.5": 0.002,
|
|
228
229
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
229
|
-
"z-ai/glm-5": 0.00208
|
|
230
|
+
"z-ai/glm-5": 0.00208,
|
|
231
|
+
"z-ai/glm-5.1": 0.0035
|
|
230
232
|
}
|
|
231
233
|
}'
|
|
232
234
|
# END_GENERATED_PRICES
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route Claude Code directly to Anthropic again (turn the Weave Router off)
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Turn the Weave Router **off** for Claude Code by running:
|
|
7
|
+
|
|
8
|
+
`npx @workweave/router off --claude{{SCOPE}}`
|
|
9
|
+
|
|
10
|
+
Then tell me to fully quit and reopen Claude Code — it only reads the router setting at startup, so the change won't apply to the current session until it restarts.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Route Claude Code through the Weave Router again (turn it back on)
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Turn the Weave Router **on** for Claude Code by running:
|
|
7
|
+
|
|
8
|
+
`npx @workweave/router on --claude{{SCOPE}}`
|
|
9
|
+
|
|
10
|
+
Then tell me to fully quit and reopen Claude Code so the change takes effect — it only reads the router setting at startup.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show whether Claude Code is routing through the Weave Router or directly to Anthropic
|
|
3
|
+
allowed-tools: Bash(npx:*)
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Report whether Claude Code is currently using the Weave Router by running:
|
|
7
|
+
|
|
8
|
+
`npx @workweave/router status --claude{{SCOPE}}`
|
|
9
|
+
|
|
10
|
+
Then summarize the result for me in one line.
|
package/install.sh
CHANGED
|
@@ -37,6 +37,18 @@
|
|
|
37
37
|
# npx @workweave/router --non-interactive # require WEAVE_ROUTER_KEY env var (defaults target to claude)
|
|
38
38
|
# npx @workweave/router --quiet # suppress banner, ping check, and trailing tips
|
|
39
39
|
# npx @workweave/router --uninstall # remove a previous install (delegates to uninstall.sh)
|
|
40
|
+
#
|
|
41
|
+
# Toggle an existing install on/off without losing the router config (so
|
|
42
|
+
# switching back is instant). These never prompt for a key and require an
|
|
43
|
+
# explicit client (--claude / --codex / --opencode); they only flip config
|
|
44
|
+
# that install.sh already wrote:
|
|
45
|
+
# npx @workweave/router off --claude # route directly to Anthropic again (Claude Code)
|
|
46
|
+
# npx @workweave/router on --codex # route through the Weave Router again (Codex)
|
|
47
|
+
# npx @workweave/router status --opencode # report whether opencode is on the router or direct
|
|
48
|
+
# Claude Code reads env at launch, so an off/on takes effect on the next
|
|
49
|
+
# `claude` start; Codex and opencode re-read config every invocation.
|
|
50
|
+
# Cursor's base URL lives in its own settings UI (no file we own), so there's
|
|
51
|
+
# nothing to toggle here — flip "Override OpenAI Base URL" in Cursor settings.
|
|
40
52
|
|
|
41
53
|
set -euo pipefail
|
|
42
54
|
|
|
@@ -65,6 +77,11 @@ router_key_header="X-Weave-Router-Key"
|
|
|
65
77
|
target="claude"
|
|
66
78
|
target_explicit="false"
|
|
67
79
|
|
|
80
|
+
# Operation mode. "install" (default) writes/refreshes config. "off"/"on"/
|
|
81
|
+
# "status" toggle or report an existing install without touching the router
|
|
82
|
+
# key/identity — see the toggle_* helpers and the dispatch block below.
|
|
83
|
+
mode="install"
|
|
84
|
+
|
|
68
85
|
# ---------- helpers ----------
|
|
69
86
|
|
|
70
87
|
# Detect whether stdout is a real terminal that grokks ANSI escapes. Pipes,
|
|
@@ -98,6 +115,29 @@ info() { printf "%s==>%s %s\n" "$C_CYAN" "$C_RESET" "$*"; }
|
|
|
98
115
|
ok() { printf "%s✓%s %s\n" "$C_GREEN" "$C_RESET" "$*"; }
|
|
99
116
|
skip() { printf "%s⊙%s %s%s%s\n" "$C_DIM" "$C_RESET" "$C_DIM" "$*" "$C_RESET"; }
|
|
100
117
|
|
|
118
|
+
# uninstall_cmd echoes the npx one-liner that reverses the current install,
|
|
119
|
+
# matching the target (--codex/--opencode), scope, and --dir so the hint we
|
|
120
|
+
# print after a successful install is copy-paste-correct. Kept in sync with
|
|
121
|
+
# uninstall.sh's flag surface.
|
|
122
|
+
uninstall_cmd() {
|
|
123
|
+
local cmd="npx -y @workweave/router --uninstall"
|
|
124
|
+
case "$target" in
|
|
125
|
+
codex) cmd="$cmd --codex" ;;
|
|
126
|
+
opencode) cmd="$cmd --opencode" ;;
|
|
127
|
+
esac
|
|
128
|
+
[ "$scope" = "project" ] && cmd="$cmd --scope project"
|
|
129
|
+
[ -n "$install_dir" ] && cmd="$cmd --dir $(printf '%q' "$install_dir")"
|
|
130
|
+
printf '%s' "$cmd"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# print_uninstall_hint prints the reverse command on its own labeled line so
|
|
134
|
+
# every successful install ends by telling the user exactly how to undo it.
|
|
135
|
+
print_uninstall_hint() {
|
|
136
|
+
[ "$quiet" = "true" ] && return 0
|
|
137
|
+
printf "%sTo uninstall:%s %s%s%s\n" \
|
|
138
|
+
"$C_BOLD" "$C_RESET" "$C_CYAN" "$(uninstall_cmd)" "$C_RESET"
|
|
139
|
+
}
|
|
140
|
+
|
|
101
141
|
# ---------- banner ----------
|
|
102
142
|
#
|
|
103
143
|
# Print the WEAVE wordmark in brand orange. Skipped under --quiet or when
|
|
@@ -537,6 +577,7 @@ write_opencode_config() {
|
|
|
537
577
|
name: "Weave Router",
|
|
538
578
|
options: { apiKey: $key, baseURL: $url, headers: $headers },
|
|
539
579
|
models: {
|
|
580
|
+
"claude-opus-4-8": { name: "Claude Opus 4.8 (via Weave Router)" },
|
|
540
581
|
"claude-opus-4-7": { name: "Claude Opus 4.7 (via Weave Router)" },
|
|
541
582
|
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6 (via Weave Router)" },
|
|
542
583
|
"claude-haiku-4-5": { name: "Claude Haiku 4.5 (via Weave Router)" }
|
|
@@ -681,6 +722,11 @@ while [ $# -gt 0 ]; do
|
|
|
681
722
|
# on the default.
|
|
682
723
|
target="claude"; target_explicit="true"; shift
|
|
683
724
|
;;
|
|
725
|
+
off|--off|on|--on|status|--status)
|
|
726
|
+
# Toggle/report verbs. Bare (off) or dashed (--off) both accepted; the
|
|
727
|
+
# npm wrapper forwards argv verbatim so either form reaches us.
|
|
728
|
+
mode="${1#--}"; shift
|
|
729
|
+
;;
|
|
684
730
|
-h|--help)
|
|
685
731
|
usage 0
|
|
686
732
|
;;
|
|
@@ -690,6 +736,17 @@ while [ $# -gt 0 ]; do
|
|
|
690
736
|
esac
|
|
691
737
|
done
|
|
692
738
|
|
|
739
|
+
# Toggle verbs only flip config install.sh already wrote: no key, no identity,
|
|
740
|
+
# no prompts. Require an explicit client so we never guess which config to
|
|
741
|
+
# touch, and suppress every interactive prompt downstream.
|
|
742
|
+
if [ "$mode" != "install" ]; then
|
|
743
|
+
non_interactive="true"
|
|
744
|
+
if [ "$target_explicit" != "true" ]; then
|
|
745
|
+
err "'$mode' requires an explicit client: --claude, --codex, or --opencode"
|
|
746
|
+
exit 2
|
|
747
|
+
fi
|
|
748
|
+
fi
|
|
749
|
+
|
|
693
750
|
if [ -z "$base_url" ]; then
|
|
694
751
|
base_url="$DEFAULT_BASE_URL"
|
|
695
752
|
fi
|
|
@@ -723,7 +780,8 @@ fi
|
|
|
723
780
|
# users see when `make full-setup` hands off to install.sh is the wordmark,
|
|
724
781
|
# not a bare "Install scope:" line. Target prompt above already finalized
|
|
725
782
|
# $target, so the banner's per-target label reflects the user's choice.
|
|
726
|
-
|
|
783
|
+
# Toggle verbs stay terse — skip the banner so `status` prints one clean line.
|
|
784
|
+
[ "$mode" = "install" ] && print_banner
|
|
727
785
|
|
|
728
786
|
# ---------- interactive scope prompt ----------
|
|
729
787
|
|
|
@@ -791,15 +849,22 @@ fi
|
|
|
791
849
|
|
|
792
850
|
# ---------- pre-flight ----------
|
|
793
851
|
|
|
794
|
-
[ "$
|
|
852
|
+
if [ "$mode" = "install" ]; then
|
|
853
|
+
[ "$quiet" = "true" ] || info "scope=${C_BOLD}${scope}${C_RESET} target=${C_BOLD}${target}${C_RESET} base_url=${C_BOLD}${base_url}${C_RESET}"
|
|
854
|
+
else
|
|
855
|
+
[ "$quiet" = "true" ] || info "mode=${C_BOLD}${mode}${C_RESET} scope=${C_BOLD}${scope}${C_RESET} target=${C_BOLD}${target}${C_RESET}"
|
|
856
|
+
fi
|
|
795
857
|
|
|
796
858
|
# Codex install only writes a TOML file (managed via awk) so jq isn't needed.
|
|
797
859
|
# Claude Code's settings.json and opencode's opencode.json patching both use
|
|
798
|
-
# jq to deep-merge / structurally rewrite JSON.
|
|
860
|
+
# jq to deep-merge / structurally rewrite JSON. Toggling those clients reads
|
|
861
|
+
# and rewrites the same JSON, so jq is required there too.
|
|
799
862
|
if [ "$target" = "claude" ] || [ "$target" = "opencode" ]; then
|
|
800
863
|
require_cmd jq "macOS: 'brew install jq' · Debian/Ubuntu: 'sudo apt install jq'"
|
|
801
864
|
fi
|
|
802
|
-
|
|
865
|
+
# curl is only used by the install path's health/validate probes; toggles never
|
|
866
|
+
# hit the network.
|
|
867
|
+
[ "$mode" = "install" ] && require_cmd curl "macOS/Linux: usually preinstalled — check your package manager"
|
|
803
868
|
|
|
804
869
|
case "$target" in
|
|
805
870
|
claude)
|
|
@@ -935,6 +1000,268 @@ else
|
|
|
935
1000
|
mkdir -p "$opencode_dir"
|
|
936
1001
|
fi
|
|
937
1002
|
|
|
1003
|
+
# ---------- off / on / status (toggle an existing install) ----------
|
|
1004
|
+
#
|
|
1005
|
+
# Flip a client between the Weave Router and talking to its provider directly,
|
|
1006
|
+
# WITHOUT discarding the router config — so switching back is one command. We
|
|
1007
|
+
# only run this for the `off`/`on`/`status` verbs; `install` falls straight
|
|
1008
|
+
# through to the write path below. An explicit client was already required
|
|
1009
|
+
# during arg parsing, so exactly one of the toggle_* helpers fires.
|
|
1010
|
+
#
|
|
1011
|
+
# Per-client "off" mechanics (each leaves the router config in place so "on"
|
|
1012
|
+
# restores it byte-for-byte):
|
|
1013
|
+
# Claude Code — moves ANTHROPIC_BASE_URL + the key header out of settings
|
|
1014
|
+
# into a parked sidecar; CC falls back to its own login.
|
|
1015
|
+
# Codex — comments the `model_provider = "weave"` line inside the
|
|
1016
|
+
# managed block; the [model_providers.weave] section stays.
|
|
1017
|
+
# opencode — parks the top-level `model` and removes it; opencode reverts
|
|
1018
|
+
# to its own default. The provider.weave block stays.
|
|
1019
|
+
# Claude Code reads env at launch, so its toggle lands on the next `claude`
|
|
1020
|
+
# start; Codex/opencode re-read config every run.
|
|
1021
|
+
|
|
1022
|
+
# router_shaped_url returns 0 when a base URL points at the router — i.e. it's
|
|
1023
|
+
# neither empty nor Anthropic's own endpoint (what "off" falls back to).
|
|
1024
|
+
router_shaped_url() {
|
|
1025
|
+
case "$1" in
|
|
1026
|
+
""|https://api.anthropic.com*|http://api.anthropic.com*) return 1 ;;
|
|
1027
|
+
*) return 0 ;;
|
|
1028
|
+
esac
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
# json_get prints a jq scalar from a file, or empty when the file/key is
|
|
1032
|
+
# absent. Never trips set -e.
|
|
1033
|
+
json_get() {
|
|
1034
|
+
[ -f "$1" ] || return 0
|
|
1035
|
+
jq -r "${2} // empty" "$1" 2>/dev/null || true
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
# gitignore_add appends an entry to the repo .gitignore in project scope so a
|
|
1039
|
+
# parked sidecar (which may carry the router key header) never gets committed.
|
|
1040
|
+
# No-op for user scope and --dir, matching how install handles its own ignores.
|
|
1041
|
+
gitignore_add() {
|
|
1042
|
+
[ "$scope" = "project" ] && [ -z "$install_dir" ] && [ -n "${git_root:-}" ] || return 0
|
|
1043
|
+
local gi="$git_root/.gitignore" entry="$1"
|
|
1044
|
+
refuse_if_symlink "$gi"
|
|
1045
|
+
if [ ! -f "$gi" ] || ! grep -qxF "$entry" "$gi"; then
|
|
1046
|
+
printf '%s\n' "$entry" >>"$gi"
|
|
1047
|
+
fi
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
toggle_claude() {
|
|
1051
|
+
local parked="$settings_dir/.weave-parked.json"
|
|
1052
|
+
local proj="false" active committed_base local_base parked_env merged
|
|
1053
|
+
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
1054
|
+
proj="true"
|
|
1055
|
+
active="$local_settings_file"
|
|
1056
|
+
else
|
|
1057
|
+
active="$settings_file"
|
|
1058
|
+
fi
|
|
1059
|
+
local parked_present="false"
|
|
1060
|
+
if [ -f "$parked" ]; then parked_present="true"; fi
|
|
1061
|
+
committed_base="$(json_get "$settings_file" '.env.ANTHROPIC_BASE_URL')"
|
|
1062
|
+
|
|
1063
|
+
case "$mode" in
|
|
1064
|
+
status)
|
|
1065
|
+
local on_hint="on --claude"
|
|
1066
|
+
[ "$proj" = "true" ] && on_hint="on --claude --scope project"
|
|
1067
|
+
if [ "$parked_present" = "true" ]; then
|
|
1068
|
+
ok "Claude Code: ${C_BOLD}off${C_RESET} — routing directly to Anthropic. Run '$on_hint' to re-enable."
|
|
1069
|
+
elif [ "$proj" = "true" ]; then
|
|
1070
|
+
local_base="$(json_get "$local_settings_file" '.env.ANTHROPIC_BASE_URL')"
|
|
1071
|
+
if [ -n "$local_base" ] && ! router_shaped_url "$local_base"; then
|
|
1072
|
+
ok "Claude Code (project): ${C_BOLD}off${C_RESET} — routing directly to Anthropic. Run '$on_hint' to re-enable."
|
|
1073
|
+
elif router_shaped_url "$committed_base"; then
|
|
1074
|
+
ok "Claude Code (project): ${C_BOLD}on${C_RESET} — routing through $committed_base."
|
|
1075
|
+
else
|
|
1076
|
+
info "Claude Code (project): not configured for the router. Run the installer first."
|
|
1077
|
+
fi
|
|
1078
|
+
elif router_shaped_url "$committed_base"; then
|
|
1079
|
+
ok "Claude Code: ${C_BOLD}on${C_RESET} — routing through $committed_base."
|
|
1080
|
+
else
|
|
1081
|
+
info "Claude Code: not configured for the router. Run the installer first."
|
|
1082
|
+
fi
|
|
1083
|
+
;;
|
|
1084
|
+
off)
|
|
1085
|
+
if [ "$parked_present" = "true" ]; then
|
|
1086
|
+
ok "Claude Code is already off — nothing to do."
|
|
1087
|
+
return 0
|
|
1088
|
+
fi
|
|
1089
|
+
if ! router_shaped_url "$committed_base"; then
|
|
1090
|
+
info "Claude Code isn't configured for the router. Run the installer first."
|
|
1091
|
+
return 0
|
|
1092
|
+
fi
|
|
1093
|
+
if [ "$proj" = "true" ]; then
|
|
1094
|
+
# Park the whole local env (carries the key header), then override the
|
|
1095
|
+
# base URL to Anthropic in the local file only — committed settings.json
|
|
1096
|
+
# is never touched, so this stays out of `git diff`.
|
|
1097
|
+
if [ -f "$local_settings_file" ]; then
|
|
1098
|
+
jq '{env: (.env // {})}' "$local_settings_file" >"$parked"
|
|
1099
|
+
merged="$(jq '.env = ((.env // {} | del(.ANTHROPIC_CUSTOM_HEADERS)) + {ANTHROPIC_BASE_URL: "https://api.anthropic.com"})' "$local_settings_file")"
|
|
1100
|
+
else
|
|
1101
|
+
printf '{"env":{}}\n' >"$parked"
|
|
1102
|
+
merged='{"env":{"ANTHROPIC_BASE_URL":"https://api.anthropic.com"}}'
|
|
1103
|
+
fi
|
|
1104
|
+
printf '%s\n' "$merged" >"$local_settings_file"
|
|
1105
|
+
chmod 600 "$local_settings_file" "$parked"
|
|
1106
|
+
gitignore_add ".claude/.weave-parked.json"
|
|
1107
|
+
else
|
|
1108
|
+
# Park just the router-owned env keys, then strip them so Claude Code
|
|
1109
|
+
# falls back to its own Anthropic login.
|
|
1110
|
+
jq '{env: ((.env // {}) | {ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS} | with_entries(select(.value != null)))}' "$settings_file" >"$parked"
|
|
1111
|
+
chmod 600 "$parked"
|
|
1112
|
+
merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL, .ANTHROPIC_CUSTOM_HEADERS)
|
|
1113
|
+
| (if (.env // {} | length) == 0 then del(.env) else . end)' "$settings_file")"
|
|
1114
|
+
printf '%s\n' "$merged" >"$settings_file"
|
|
1115
|
+
fi
|
|
1116
|
+
ok "Claude Code is now ${C_BOLD}off${C_RESET} (direct to Anthropic). Restart Claude Code for it to take effect."
|
|
1117
|
+
;;
|
|
1118
|
+
on)
|
|
1119
|
+
if [ "$parked_present" != "true" ]; then
|
|
1120
|
+
# Project scope can still carry a direct-override in settings.local.json
|
|
1121
|
+
# even with no sidecar (e.g. the parked file was deleted by hand). That
|
|
1122
|
+
# override wins over the committed router URL, so traffic is really
|
|
1123
|
+
# going direct — drop it so "on" matches what "status" reports, instead
|
|
1124
|
+
# of falsely claiming we're already on.
|
|
1125
|
+
if [ "$proj" = "true" ]; then
|
|
1126
|
+
local_base="$(json_get "$local_settings_file" '.env.ANTHROPIC_BASE_URL')"
|
|
1127
|
+
if [ -n "$local_base" ] && ! router_shaped_url "$local_base"; then
|
|
1128
|
+
merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL)
|
|
1129
|
+
| (if (.env // {} | length) == 0 then del(.env) else . end)' "$local_settings_file")"
|
|
1130
|
+
printf '%s\n' "$merged" >"$local_settings_file"
|
|
1131
|
+
chmod 600 "$local_settings_file"
|
|
1132
|
+
ok "Claude Code is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
|
|
1133
|
+
return 0
|
|
1134
|
+
fi
|
|
1135
|
+
fi
|
|
1136
|
+
if router_shaped_url "$committed_base"; then
|
|
1137
|
+
ok "Claude Code is already on — nothing to do."
|
|
1138
|
+
else
|
|
1139
|
+
warn "No parked router config found. Run the installer to set up Claude Code."
|
|
1140
|
+
fi
|
|
1141
|
+
return 0
|
|
1142
|
+
fi
|
|
1143
|
+
parked_env="$(jq '.env' "$parked")"
|
|
1144
|
+
merged="$(jq --argjson p "$parked_env" '.env = (((.env // {}) | del(.ANTHROPIC_BASE_URL)) + $p)' "$active")"
|
|
1145
|
+
printf '%s\n' "$merged" >"$active"
|
|
1146
|
+
[ "$proj" = "true" ] && chmod 600 "$active"
|
|
1147
|
+
rm -f "$parked"
|
|
1148
|
+
ok "Claude Code is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
|
|
1149
|
+
;;
|
|
1150
|
+
esac
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
toggle_codex() {
|
|
1154
|
+
local f="$codex_config_file" state="absent" tmp
|
|
1155
|
+
if [ -f "$f" ]; then
|
|
1156
|
+
state="$(awk -v b="$WEAVE_CODEX_BEGIN_MARKER" -v e="$WEAVE_CODEX_END_MARKER" '
|
|
1157
|
+
$0==b{inblk=1; next}
|
|
1158
|
+
$0==e{inblk=0; next}
|
|
1159
|
+
inblk && /^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"/ {st="on"}
|
|
1160
|
+
inblk && /^[[:space:]]*#[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"/ {if(st=="")st="off"}
|
|
1161
|
+
END{print (st==""?"absent":st)}
|
|
1162
|
+
' "$f")"
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
case "$mode" in
|
|
1166
|
+
status)
|
|
1167
|
+
case "$state" in
|
|
1168
|
+
on) ok "Codex: ${C_BOLD}on${C_RESET} — routing through the Weave Router." ;;
|
|
1169
|
+
off) ok "Codex: ${C_BOLD}off${C_RESET} — using Codex's default provider. Run 'on --codex' to re-enable." ;;
|
|
1170
|
+
*) info "Codex: not configured for the router. Run the installer first." ;;
|
|
1171
|
+
esac
|
|
1172
|
+
;;
|
|
1173
|
+
off)
|
|
1174
|
+
if [ "$state" = "absent" ]; then info "Codex isn't configured for the router. Run the installer first."; return 0; fi
|
|
1175
|
+
if [ "$state" = "off" ]; then ok "Codex is already off — nothing to do."; return 0; fi
|
|
1176
|
+
tmp="$(mktemp -t weave-codex-toggle.XXXXXX)"
|
|
1177
|
+
awk -v b="$WEAVE_CODEX_BEGIN_MARKER" -v e="$WEAVE_CODEX_END_MARKER" '
|
|
1178
|
+
$0==b{inblk=1; print; next}
|
|
1179
|
+
$0==e{inblk=0; print; next}
|
|
1180
|
+
inblk && /^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"[[:space:]]*$/ {
|
|
1181
|
+
print "# " $0 " # weave-router: off (run on to re-enable)"; next
|
|
1182
|
+
}
|
|
1183
|
+
{print}
|
|
1184
|
+
' "$f" >"$tmp" && mv "$tmp" "$f"
|
|
1185
|
+
chmod 600 "$f"
|
|
1186
|
+
ok "Codex is now ${C_BOLD}off${C_RESET} (default provider). Takes effect on your next 'codex' run."
|
|
1187
|
+
;;
|
|
1188
|
+
on)
|
|
1189
|
+
if [ "$state" = "absent" ]; then warn "No managed Weave block in $f. Run the installer to set up Codex."; return 0; fi
|
|
1190
|
+
if [ "$state" = "on" ]; then ok "Codex is already on — nothing to do."; return 0; fi
|
|
1191
|
+
tmp="$(mktemp -t weave-codex-toggle.XXXXXX)"
|
|
1192
|
+
awk -v b="$WEAVE_CODEX_BEGIN_MARKER" -v e="$WEAVE_CODEX_END_MARKER" '
|
|
1193
|
+
$0==b{inblk=1; print; next}
|
|
1194
|
+
$0==e{inblk=0; print; next}
|
|
1195
|
+
inblk && /^[[:space:]]*#[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"/ {
|
|
1196
|
+
print "model_provider = \"weave\""; next
|
|
1197
|
+
}
|
|
1198
|
+
{print}
|
|
1199
|
+
' "$f" >"$tmp" && mv "$tmp" "$f"
|
|
1200
|
+
chmod 600 "$f"
|
|
1201
|
+
ok "Codex is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Takes effect on your next 'codex' run."
|
|
1202
|
+
;;
|
|
1203
|
+
esac
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
toggle_opencode() {
|
|
1207
|
+
local f="$opencode_config_file" parked="$opencode_dir/.weave-parked.json"
|
|
1208
|
+
local model="" has_weave="false" parked_present="false" on="false" restore_model merged
|
|
1209
|
+
if [ -f "$parked" ]; then parked_present="true"; fi
|
|
1210
|
+
if [ -f "$f" ]; then
|
|
1211
|
+
model="$(jq -r '.model // empty' "$f" 2>/dev/null || true)"
|
|
1212
|
+
if [ "$(jq -r '((.provider // {}) | has("weave"))' "$f" 2>/dev/null || true)" = "true" ]; then has_weave="true"; fi
|
|
1213
|
+
fi
|
|
1214
|
+
case "$model" in weave/*) on="true" ;; esac
|
|
1215
|
+
|
|
1216
|
+
case "$mode" in
|
|
1217
|
+
status)
|
|
1218
|
+
if [ "$on" = "true" ]; then
|
|
1219
|
+
ok "opencode: ${C_BOLD}on${C_RESET} — default model is $model (via the Weave Router)."
|
|
1220
|
+
elif [ "$has_weave" = "true" ] || [ "$parked_present" = "true" ]; then
|
|
1221
|
+
ok "opencode: ${C_BOLD}off${C_RESET} — not using the Weave Router model. Run 'on --opencode' to re-enable."
|
|
1222
|
+
else
|
|
1223
|
+
info "opencode: not configured for the router. Run the installer first."
|
|
1224
|
+
fi
|
|
1225
|
+
;;
|
|
1226
|
+
off)
|
|
1227
|
+
if [ "$on" != "true" ]; then
|
|
1228
|
+
if [ "$has_weave" = "true" ]; then ok "opencode is already off — nothing to do."; else info "opencode isn't configured for the router. Run the installer first."; fi
|
|
1229
|
+
return 0
|
|
1230
|
+
fi
|
|
1231
|
+
jq '{model: .model}' "$f" >"$parked"
|
|
1232
|
+
chmod 600 "$parked"
|
|
1233
|
+
merged="$(jq 'del(.model)' "$f")"
|
|
1234
|
+
printf '%s\n' "$merged" >"$f"
|
|
1235
|
+
chmod 600 "$f"
|
|
1236
|
+
gitignore_add ".weave-parked.json"
|
|
1237
|
+
ok "opencode is now ${C_BOLD}off${C_RESET} — pick a non-Weave model with /models. Takes effect on your next opencode run."
|
|
1238
|
+
;;
|
|
1239
|
+
on)
|
|
1240
|
+
if [ "$on" = "true" ]; then ok "opencode is already on — nothing to do."; return 0; fi
|
|
1241
|
+
restore_model="weave/claude-sonnet-4-6"
|
|
1242
|
+
if [ "$parked_present" = "true" ]; then
|
|
1243
|
+
restore_model="$(jq -r '.model // "weave/claude-sonnet-4-6"' "$parked")"
|
|
1244
|
+
elif [ "$has_weave" != "true" ]; then
|
|
1245
|
+
warn "opencode isn't configured for the router. Run the installer first."; return 0
|
|
1246
|
+
fi
|
|
1247
|
+
merged="$(jq --arg m "$restore_model" '.model = $m' "$f")"
|
|
1248
|
+
printf '%s\n' "$merged" >"$f"
|
|
1249
|
+
chmod 600 "$f"
|
|
1250
|
+
rm -f "$parked"
|
|
1251
|
+
ok "opencode is now ${C_BOLD}on${C_RESET} (default model $restore_model via the Weave Router). Takes effect on your next opencode run."
|
|
1252
|
+
;;
|
|
1253
|
+
esac
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if [ "$mode" != "install" ]; then
|
|
1257
|
+
case "$target" in
|
|
1258
|
+
claude) toggle_claude ;;
|
|
1259
|
+
codex) toggle_codex ;;
|
|
1260
|
+
opencode) toggle_opencode ;;
|
|
1261
|
+
esac
|
|
1262
|
+
exit 0
|
|
1263
|
+
fi
|
|
1264
|
+
|
|
938
1265
|
# ---------- token handling ----------
|
|
939
1266
|
|
|
940
1267
|
api_key=""
|
|
@@ -1027,16 +1354,44 @@ install_slash_commands() {
|
|
|
1027
1354
|
refuse_if_symlink "$dst_dir"
|
|
1028
1355
|
fi
|
|
1029
1356
|
mkdir -p "$dst_dir"
|
|
1030
|
-
|
|
1357
|
+
|
|
1358
|
+
# force-model/unforce-model are router-intercepted prompt expansions and
|
|
1359
|
+
# apply to every target. The router-off/on/status wrappers shell out to this
|
|
1360
|
+
# installer to flip the *local* config, so they're Claude Code-only and need
|
|
1361
|
+
# the install scope baked into the command (the .md can't discover it at
|
|
1362
|
+
# invocation time). {{SCOPE}} is substituted accordingly.
|
|
1363
|
+
installed="force-model, unforce-model"
|
|
1364
|
+
cmds="force-model unforce-model"
|
|
1365
|
+
if [ "$target" = "claude" ]; then
|
|
1366
|
+
cmds="$cmds router-off router-on router-status"
|
|
1367
|
+
installed="$installed, router-off, router-on, router-status"
|
|
1368
|
+
fi
|
|
1369
|
+
|
|
1370
|
+
# Bake the same scope selector the toggle needs to find this install: --dir
|
|
1371
|
+
# overrides scope (mirrors install/uninstall path resolution), so a sandbox
|
|
1372
|
+
# install gets `--dir <path>` and the slash commands flip that dir rather
|
|
1373
|
+
# than the default user-scope paths. printf %q quotes paths with spaces.
|
|
1374
|
+
local scope_args=""
|
|
1375
|
+
if [ -n "$install_dir" ]; then
|
|
1376
|
+
scope_args=" --dir $(printf '%q' "$install_dir")"
|
|
1377
|
+
elif [ "$scope" = "project" ]; then
|
|
1378
|
+
scope_args=" --scope project"
|
|
1379
|
+
fi
|
|
1380
|
+
|
|
1381
|
+
for cmd in $cmds; do
|
|
1031
1382
|
src="$commands_src_dir/$cmd.md"
|
|
1032
1383
|
dst="$dst_dir/$cmd.md"
|
|
1033
1384
|
[ -f "$src" ] || continue
|
|
1034
1385
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1035
1386
|
refuse_if_symlink "$dst"
|
|
1036
1387
|
fi
|
|
1037
|
-
|
|
1388
|
+
# Substitute the {{SCOPE}} placeholder (only the router-* wrappers carry it;
|
|
1389
|
+
# cp-equivalent for the others since the token is absent).
|
|
1390
|
+
local body; body="$(cat "$src")"
|
|
1391
|
+
body="${body//\{\{SCOPE\}\}/$scope_args}"
|
|
1392
|
+
printf '%s\n' "$body" >"$dst"
|
|
1038
1393
|
done
|
|
1039
|
-
ok "Slash commands written to $dst_dir (
|
|
1394
|
+
ok "Slash commands written to $dst_dir ($installed)"
|
|
1040
1395
|
}
|
|
1041
1396
|
|
|
1042
1397
|
# ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
|
|
@@ -1091,6 +1446,7 @@ if [ "$target" = "codex" ]; then
|
|
|
1091
1446
|
# point CODEX_HOME at the directory we wrote so Codex finds our config.
|
|
1092
1447
|
info "Run Codex with CODEX_HOME=$codex_dir codex so it picks up this config."
|
|
1093
1448
|
fi
|
|
1449
|
+
print_uninstall_hint
|
|
1094
1450
|
exit 0
|
|
1095
1451
|
fi
|
|
1096
1452
|
|
|
@@ -1141,6 +1497,7 @@ if [ "$target" = "opencode" ]; then
|
|
|
1141
1497
|
# has to point opencode at the file explicitly.
|
|
1142
1498
|
info "Run opencode with OPENCODE_CONFIG=$opencode_config_file opencode."
|
|
1143
1499
|
fi
|
|
1500
|
+
print_uninstall_hint
|
|
1144
1501
|
exit 0
|
|
1145
1502
|
fi
|
|
1146
1503
|
|
|
@@ -1279,8 +1636,9 @@ normalize_model() {
|
|
|
1279
1636
|
prices='{
|
|
1280
1637
|
"input": {
|
|
1281
1638
|
"claude-haiku-4-5": 0.0008,
|
|
1282
|
-
"claude-opus-4-6": 0.
|
|
1283
|
-
"claude-opus-4-7": 0.
|
|
1639
|
+
"claude-opus-4-6": 0.005,
|
|
1640
|
+
"claude-opus-4-7": 0.005,
|
|
1641
|
+
"claude-opus-4-8": 0.005,
|
|
1284
1642
|
"claude-sonnet-4-5": 0.003,
|
|
1285
1643
|
"claude-sonnet-4-6": 0.003,
|
|
1286
1644
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
@@ -1316,21 +1674,22 @@ prices='{
|
|
|
1316
1674
|
"mistralai/mistral-small-2603": 0.0002,
|
|
1317
1675
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
1318
1676
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
1319
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
1677
|
+
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
1320
1678
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
1321
1679
|
"qwen/qwen3-coder": 0.0009,
|
|
1322
1680
|
"qwen/qwen3-coder-next": 0.0005,
|
|
1323
1681
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
1324
1682
|
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
1325
1683
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
1326
|
-
"xiaomi/mimo-v2.5": 0.0004,
|
|
1327
1684
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
1328
|
-
"z-ai/glm-5": 0.0006
|
|
1685
|
+
"z-ai/glm-5": 0.0006,
|
|
1686
|
+
"z-ai/glm-5.1": 0.00105
|
|
1329
1687
|
},
|
|
1330
1688
|
"output": {
|
|
1331
1689
|
"claude-haiku-4-5": 0.004,
|
|
1332
|
-
"claude-opus-4-6": 0.
|
|
1333
|
-
"claude-opus-4-7": 0.
|
|
1690
|
+
"claude-opus-4-6": 0.025,
|
|
1691
|
+
"claude-opus-4-7": 0.025,
|
|
1692
|
+
"claude-opus-4-8": 0.025,
|
|
1334
1693
|
"claude-sonnet-4-5": 0.015,
|
|
1335
1694
|
"claude-sonnet-4-6": 0.015,
|
|
1336
1695
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
@@ -1366,16 +1725,16 @@ prices='{
|
|
|
1366
1725
|
"mistralai/mistral-small-2603": 0.0006,
|
|
1367
1726
|
"moonshotai/kimi-k2.5": 0.003,
|
|
1368
1727
|
"moonshotai/kimi-k2.6": 0.004,
|
|
1369
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
1728
|
+
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
1370
1729
|
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
1371
1730
|
"qwen/qwen3-coder": 0.0027,
|
|
1372
1731
|
"qwen/qwen3-coder-next": 0.0012,
|
|
1373
1732
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
1374
1733
|
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
1375
1734
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
1376
|
-
"xiaomi/mimo-v2.5": 0.002,
|
|
1377
1735
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
1378
|
-
"z-ai/glm-5": 0.00208
|
|
1736
|
+
"z-ai/glm-5": 0.00208,
|
|
1737
|
+
"z-ai/glm-5.1": 0.0035
|
|
1379
1738
|
}
|
|
1380
1739
|
}'
|
|
1381
1740
|
# END_GENERATED_PRICES
|
|
@@ -1676,3 +2035,4 @@ fi
|
|
|
1676
2035
|
printf "\n"
|
|
1677
2036
|
printf "%s✓%s %s%sWeave Router installed for Claude Code.%s\n" \
|
|
1678
2037
|
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
2038
|
+
print_uninstall_hint
|
package/package.json
CHANGED
package/uninstall.sh
CHANGED
|
@@ -173,6 +173,14 @@ if [ "$target" = "opencode" ]; then
|
|
|
173
173
|
info "No opencode config at $opencode_config_file (already uninstalled?)"
|
|
174
174
|
fi
|
|
175
175
|
|
|
176
|
+
# Drop the toggle parked sidecar (holds the parked router model when off).
|
|
177
|
+
opencode_parked="$opencode_dir/.weave-parked.json"
|
|
178
|
+
if [ -f "$opencode_parked" ]; then
|
|
179
|
+
refuse_if_symlink "$opencode_parked"
|
|
180
|
+
rm -f "$opencode_parked"
|
|
181
|
+
ok "Removed $opencode_parked"
|
|
182
|
+
fi
|
|
183
|
+
|
|
176
184
|
if [ -n "$install_dir" ]; then
|
|
177
185
|
ok "Weave Router uninstalled from $install_dir (opencode)."
|
|
178
186
|
else
|
|
@@ -351,9 +359,12 @@ else
|
|
|
351
359
|
fi
|
|
352
360
|
|
|
353
361
|
if [ -n "$local_settings_file" ] && [ -f "$local_settings_file" ]; then
|
|
362
|
+
# ANTHROPIC_BASE_URL only lives here when a project install was toggled off
|
|
363
|
+
# (the off path overrides it to Anthropic in the local file); scrub it too so
|
|
364
|
+
# uninstall fully reverts a toggled-off install.
|
|
354
365
|
cleaned="$(jq '
|
|
355
366
|
if .env then
|
|
356
|
-
.env |= (del(.ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS))
|
|
367
|
+
.env |= (del(.ANTHROPIC_BASE_URL, .ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS))
|
|
357
368
|
| (if (.env | length) == 0 then del(.env) else . end)
|
|
358
369
|
else . end
|
|
359
370
|
| del(.apiKeyHelper)
|
|
@@ -362,6 +373,14 @@ if [ -n "$local_settings_file" ] && [ -f "$local_settings_file" ]; then
|
|
|
362
373
|
ok "Cleaned $local_settings_file"
|
|
363
374
|
fi
|
|
364
375
|
|
|
376
|
+
# Drop the toggle parked sidecar (carries the router key header when off).
|
|
377
|
+
parked_file="$(dirname "$settings_file")/.weave-parked.json"
|
|
378
|
+
if [ -f "$parked_file" ]; then
|
|
379
|
+
refuse_if_symlink "$parked_file"
|
|
380
|
+
rm -f "$parked_file"
|
|
381
|
+
ok "Removed $parked_file"
|
|
382
|
+
fi
|
|
383
|
+
|
|
365
384
|
for f in "$statusline_file"; do
|
|
366
385
|
if [ -f "$f" ]; then
|
|
367
386
|
rm -f "$f"
|
|
@@ -375,7 +394,7 @@ done
|
|
|
375
394
|
commands_dir="$(dirname "$settings_file")/commands"
|
|
376
395
|
if [ -d "$commands_dir" ]; then
|
|
377
396
|
refuse_if_symlink "$commands_dir"
|
|
378
|
-
for cmd in force-model unforce-model; do
|
|
397
|
+
for cmd in force-model unforce-model router-off router-on router-status; do
|
|
379
398
|
cmd_file="$commands_dir/$cmd.md"
|
|
380
399
|
if [ -f "$cmd_file" ]; then
|
|
381
400
|
refuse_if_symlink "$cmd_file"
|