@workweave/router 0.1.6 → 0.1.8

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
@@ -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.015,
134
- "claude-opus-4-7": 0.015,
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.000071,
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.075,
184
- "claude-opus-4-7": 0.075,
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.000463,
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
- print_banner
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
- [ "$quiet" = "true" ] || info "scope=${C_BOLD}${scope}${C_RESET} target=${C_BOLD}${target}${C_RESET} base_url=${C_BOLD}${base_url}${C_RESET}"
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
- require_cmd curl "macOS/Linux: usually preinstalled check your package manager"
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,348 @@ 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
+ # claude_key_present returns 0 when the given settings file's
1039
+ # env.ANTHROPIC_CUSTOM_HEADERS carries the router key header. "On" is only valid
1040
+ # when this is true: in project scope the committed settings.json holds the
1041
+ # router URL but the key header lives only in the per-teammate settings.local.json
1042
+ # (or the parked sidecar), so a router URL alone doesn't mean requests can
1043
+ # authenticate.
1044
+ claude_key_present() {
1045
+ case "$(json_get "$1" '.env.ANTHROPIC_CUSTOM_HEADERS')" in
1046
+ *X-Weave-Router-Key*) return 0 ;;
1047
+ *) return 1 ;;
1048
+ esac
1049
+ }
1050
+
1051
+ # gitignore_add appends an entry to the repo .gitignore in project scope so a
1052
+ # parked sidecar (which may carry the router key header) never gets committed.
1053
+ # No-op for user scope and --dir, matching how install handles its own ignores.
1054
+ gitignore_add() {
1055
+ [ "$scope" = "project" ] && [ -z "$install_dir" ] && [ -n "${git_root:-}" ] || return 0
1056
+ local gi="$git_root/.gitignore" entry="$1"
1057
+ refuse_if_symlink "$gi"
1058
+ if [ ! -f "$gi" ] || ! grep -qxF "$entry" "$gi"; then
1059
+ printf '%s\n' "$entry" >>"$gi"
1060
+ fi
1061
+ }
1062
+
1063
+ toggle_claude() {
1064
+ local parked="$settings_dir/.weave-parked.json"
1065
+ local proj="false" active committed_base local_base parked_env merged
1066
+ if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
1067
+ proj="true"
1068
+ active="$local_settings_file"
1069
+ else
1070
+ active="$settings_file"
1071
+ fi
1072
+ # Symlink containment for the parked sidecar: project/--dir paths come from a
1073
+ # possibly-hostile repo, and `off` writes $parked via shell redirection (which
1074
+ # follows symlinks). A repo could pre-place it as a symlink to clobber an
1075
+ # arbitrary file or siphon the router-key-bearing parked data. The config
1076
+ # files themselves are already guarded during path resolution; this covers the
1077
+ # sidecar. User scope ($HOME) is trusted, matching the installer.
1078
+ if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
1079
+ refuse_if_symlink "$parked"
1080
+ fi
1081
+ local parked_present="false"
1082
+ if [ -f "$parked" ]; then parked_present="true"; fi
1083
+ committed_base="$(json_get "$settings_file" '.env.ANTHROPIC_BASE_URL')"
1084
+
1085
+ case "$mode" in
1086
+ status)
1087
+ local on_hint="on --claude"
1088
+ [ "$proj" = "true" ] && on_hint="on --claude --scope project"
1089
+ if [ "$parked_present" = "true" ]; then
1090
+ ok "Claude Code: ${C_BOLD}off${C_RESET} — routing directly to Anthropic. Run '$on_hint' to re-enable."
1091
+ elif [ "$proj" = "true" ]; then
1092
+ local_base="$(json_get "$local_settings_file" '.env.ANTHROPIC_BASE_URL')"
1093
+ if [ -n "$local_base" ] && ! router_shaped_url "$local_base"; then
1094
+ ok "Claude Code (project): ${C_BOLD}off${C_RESET} — routing directly to Anthropic. Run '$on_hint' to re-enable."
1095
+ elif router_shaped_url "$committed_base"; then
1096
+ # Router URL is committed, but it only authenticates if this teammate's
1097
+ # settings.local.json carries the key header. A fresh clone (shared
1098
+ # settings.json, no personal local file) has the URL but no key.
1099
+ if claude_key_present "$local_settings_file"; then
1100
+ ok "Claude Code (project): ${C_BOLD}on${C_RESET} — routing through $committed_base."
1101
+ else
1102
+ warn "Claude Code (project): router URL is set but your personal router key is missing (no settings.local.json) — requests won't authenticate. Run the installer to add your key."
1103
+ fi
1104
+ else
1105
+ info "Claude Code (project): not configured for the router. Run the installer first."
1106
+ fi
1107
+ elif router_shaped_url "$committed_base"; then
1108
+ if claude_key_present "$settings_file"; then
1109
+ ok "Claude Code: ${C_BOLD}on${C_RESET} — routing through $committed_base."
1110
+ else
1111
+ warn "Claude Code: router URL is set but the router key header is missing — requests won't authenticate. Run the installer to restore it."
1112
+ fi
1113
+ else
1114
+ info "Claude Code: not configured for the router. Run the installer first."
1115
+ fi
1116
+ ;;
1117
+ off)
1118
+ if [ "$parked_present" = "true" ]; then
1119
+ ok "Claude Code is already off — nothing to do."
1120
+ return 0
1121
+ fi
1122
+ if ! router_shaped_url "$committed_base"; then
1123
+ info "Claude Code isn't configured for the router. Run the installer first."
1124
+ return 0
1125
+ fi
1126
+ if [ "$proj" = "true" ]; then
1127
+ # Park the whole local env (carries the key header), then override the
1128
+ # base URL to Anthropic in the local file only — committed settings.json
1129
+ # is never touched, so this stays out of `git diff`.
1130
+ if [ -f "$local_settings_file" ]; then
1131
+ jq '{env: (.env // {})}' "$local_settings_file" >"$parked"
1132
+ merged="$(jq '.env = ((.env // {} | del(.ANTHROPIC_CUSTOM_HEADERS)) + {ANTHROPIC_BASE_URL: "https://api.anthropic.com"})' "$local_settings_file")"
1133
+ else
1134
+ printf '{"env":{}}\n' >"$parked"
1135
+ merged='{"env":{"ANTHROPIC_BASE_URL":"https://api.anthropic.com"}}'
1136
+ fi
1137
+ printf '%s\n' "$merged" >"$local_settings_file"
1138
+ chmod 600 "$local_settings_file" "$parked"
1139
+ gitignore_add ".claude/.weave-parked.json"
1140
+ else
1141
+ # Park just the router-owned env keys, then strip them so Claude Code
1142
+ # falls back to its own Anthropic login.
1143
+ jq '{env: ((.env // {}) | {ANTHROPIC_BASE_URL, ANTHROPIC_CUSTOM_HEADERS} | with_entries(select(.value != null)))}' "$settings_file" >"$parked"
1144
+ chmod 600 "$parked"
1145
+ merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL, .ANTHROPIC_CUSTOM_HEADERS)
1146
+ | (if (.env // {} | length) == 0 then del(.env) else . end)' "$settings_file")"
1147
+ printf '%s\n' "$merged" >"$settings_file"
1148
+ fi
1149
+ ok "Claude Code is now ${C_BOLD}off${C_RESET} (direct to Anthropic). Restart Claude Code for it to take effect."
1150
+ ;;
1151
+ on)
1152
+ if [ "$parked_present" != "true" ]; then
1153
+ # Project scope can still carry a direct-override in settings.local.json
1154
+ # even with no sidecar (e.g. the parked file was deleted by hand). That
1155
+ # override wins over the committed router URL, so traffic is really
1156
+ # going direct — drop it so "on" matches what "status" reports, instead
1157
+ # of falsely claiming we're already on.
1158
+ if [ "$proj" = "true" ]; then
1159
+ local_base="$(json_get "$local_settings_file" '.env.ANTHROPIC_BASE_URL')"
1160
+ if [ -n "$local_base" ] && ! router_shaped_url "$local_base"; then
1161
+ # We're off, but the parked sidecar is gone. The router key header
1162
+ # lives only in the local file / sidecar in project scope — never in
1163
+ # committed settings.json — so we can only re-enable cleanly if the
1164
+ # header survived in the local file. If it didn't, clearing the
1165
+ # override would point Claude Code at the router with no auth
1166
+ # (401s); leave the working direct setup in place and tell the user
1167
+ # to reinstall instead of faking success.
1168
+ if claude_key_present "$local_settings_file"; then
1169
+ merged="$(jq '(.env // {}) |= del(.ANTHROPIC_BASE_URL)
1170
+ | (if (.env // {} | length) == 0 then del(.env) else . end)' "$local_settings_file")"
1171
+ printf '%s\n' "$merged" >"$local_settings_file"
1172
+ chmod 600 "$local_settings_file"
1173
+ ok "Claude Code is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
1174
+ else
1175
+ warn "Claude Code is off and the parked router key is missing (its sidecar was deleted). Re-run the installer to restore the router key — leaving the current direct-to-Anthropic setup in place so requests don't fail auth."
1176
+ fi
1177
+ return 0
1178
+ fi
1179
+ fi
1180
+ # No direct override (or user scope). "On" requires both the router URL
1181
+ # and the key header — a committed router URL with no local key (e.g. a
1182
+ # fresh clone) can't authenticate, so don't claim it's already on.
1183
+ if ! router_shaped_url "$committed_base"; then
1184
+ warn "No parked router config found. Run the installer to set up Claude Code."
1185
+ elif claude_key_present "$active"; then
1186
+ ok "Claude Code is already on — nothing to do."
1187
+ else
1188
+ # $active is settings.local.json in project scope, settings.json for
1189
+ # user/--dir — name the right file so the hint isn't misleading.
1190
+ warn "Router URL is set but the router key is missing — run the installer to add your key (written to $(basename "$active"))."
1191
+ fi
1192
+ return 0
1193
+ fi
1194
+ # Sidecar present: restore it — but only if the result actually carries
1195
+ # the router key. An off that ran with an empty/absent settings.local.json
1196
+ # parks {"env":{}}; blindly restoring that would drop the direct override
1197
+ # and leave the committed router URL unauthenticated while printing
1198
+ # success. Refuse that and tell the user to reinstall.
1199
+ parked_env="$(jq -c '.env // {}' "$parked")"
1200
+ parked_has_key="false"
1201
+ if printf '%s' "$parked_env" | jq -e '(.ANTHROPIC_CUSTOM_HEADERS // "") | test("X-Weave-Router-Key")' >/dev/null 2>&1; then
1202
+ parked_has_key="true"
1203
+ fi
1204
+ if [ "$parked_has_key" != "true" ] && ! claude_key_present "$active"; then
1205
+ warn "Can't re-enable: the parked config has no router key (it was created without one). Re-run the installer to set up your router key — leaving the current direct-to-Anthropic setup in place."
1206
+ return 0
1207
+ fi
1208
+ merged="$(jq --argjson p "$parked_env" '.env = (((.env // {}) | del(.ANTHROPIC_BASE_URL)) + $p)' "$active")"
1209
+ printf '%s\n' "$merged" >"$active"
1210
+ [ "$proj" = "true" ] && chmod 600 "$active"
1211
+ rm -f "$parked"
1212
+ ok "Claude Code is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Restart Claude Code for it to take effect."
1213
+ ;;
1214
+ esac
1215
+ }
1216
+
1217
+ toggle_codex() {
1218
+ local f="$codex_config_file" state="absent" tmp
1219
+ if [ -f "$f" ]; then
1220
+ state="$(awk -v b="$WEAVE_CODEX_BEGIN_MARKER" -v e="$WEAVE_CODEX_END_MARKER" '
1221
+ $0==b{inblk=1; next}
1222
+ $0==e{inblk=0; next}
1223
+ inblk && /^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"/ {st="on"}
1224
+ inblk && /^[[:space:]]*#[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"/ {if(st=="")st="off"}
1225
+ END{print (st==""?"absent":st)}
1226
+ ' "$f")"
1227
+ fi
1228
+
1229
+ case "$mode" in
1230
+ status)
1231
+ case "$state" in
1232
+ on) ok "Codex: ${C_BOLD}on${C_RESET} — routing through the Weave Router." ;;
1233
+ off) ok "Codex: ${C_BOLD}off${C_RESET} — using Codex's default provider. Run 'on --codex' to re-enable." ;;
1234
+ *) info "Codex: not configured for the router. Run the installer first." ;;
1235
+ esac
1236
+ ;;
1237
+ off)
1238
+ if [ "$state" = "absent" ]; then info "Codex isn't configured for the router. Run the installer first."; return 0; fi
1239
+ if [ "$state" = "off" ]; then ok "Codex is already off — nothing to do."; return 0; fi
1240
+ tmp="$(mktemp -t weave-codex-toggle.XXXXXX)"
1241
+ awk -v b="$WEAVE_CODEX_BEGIN_MARKER" -v e="$WEAVE_CODEX_END_MARKER" '
1242
+ $0==b{inblk=1; print; next}
1243
+ $0==e{inblk=0; print; next}
1244
+ inblk && /^[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"[[:space:]]*$/ {
1245
+ print "# " $0 " # weave-router: off (run on to re-enable)"; next
1246
+ }
1247
+ {print}
1248
+ ' "$f" >"$tmp" && mv "$tmp" "$f"
1249
+ chmod 600 "$f"
1250
+ ok "Codex is now ${C_BOLD}off${C_RESET} (default provider). Takes effect on your next 'codex' run."
1251
+ ;;
1252
+ on)
1253
+ if [ "$state" = "absent" ]; then warn "No managed Weave block in $f. Run the installer to set up Codex."; return 0; fi
1254
+ if [ "$state" = "on" ]; then ok "Codex is already on — nothing to do."; return 0; fi
1255
+ tmp="$(mktemp -t weave-codex-toggle.XXXXXX)"
1256
+ awk -v b="$WEAVE_CODEX_BEGIN_MARKER" -v e="$WEAVE_CODEX_END_MARKER" '
1257
+ $0==b{inblk=1; print; next}
1258
+ $0==e{inblk=0; print; next}
1259
+ inblk && /^[[:space:]]*#[[:space:]]*model_provider[[:space:]]*=[[:space:]]*"weave"/ {
1260
+ print "model_provider = \"weave\""; next
1261
+ }
1262
+ {print}
1263
+ ' "$f" >"$tmp" && mv "$tmp" "$f"
1264
+ chmod 600 "$f"
1265
+ ok "Codex is now ${C_BOLD}on${C_RESET} (routing through the Weave Router). Takes effect on your next 'codex' run."
1266
+ ;;
1267
+ esac
1268
+ }
1269
+
1270
+ toggle_opencode() {
1271
+ local f="$opencode_config_file" parked="$opencode_dir/.weave-parked.json"
1272
+ local model="" has_weave="false" parked_present="false" on="false" restore_model merged
1273
+ # Symlink containment for the parked sidecar — `off` writes it via shell
1274
+ # redirection; a hostile project repo could pre-place it as a symlink. The
1275
+ # opencode.json itself is already guarded during path resolution.
1276
+ if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
1277
+ refuse_if_symlink "$parked"
1278
+ fi
1279
+ if [ -f "$parked" ]; then parked_present="true"; fi
1280
+ if [ -f "$f" ]; then
1281
+ model="$(jq -r '.model // empty' "$f" 2>/dev/null || true)"
1282
+ if [ "$(jq -r '((.provider // {}) | has("weave"))' "$f" 2>/dev/null || true)" = "true" ]; then has_weave="true"; fi
1283
+ fi
1284
+ case "$model" in weave/*) on="true" ;; esac
1285
+
1286
+ case "$mode" in
1287
+ status)
1288
+ if [ "$on" = "true" ]; then
1289
+ ok "opencode: ${C_BOLD}on${C_RESET} — default model is $model (via the Weave Router)."
1290
+ elif [ "$has_weave" = "true" ] || [ "$parked_present" = "true" ]; then
1291
+ ok "opencode: ${C_BOLD}off${C_RESET} — not using the Weave Router model. Run 'on --opencode' to re-enable."
1292
+ else
1293
+ info "opencode: not configured for the router. Run the installer first."
1294
+ fi
1295
+ ;;
1296
+ off)
1297
+ if [ "$on" != "true" ]; then
1298
+ 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
1299
+ return 0
1300
+ fi
1301
+ jq '{model: .model}' "$f" >"$parked"
1302
+ chmod 600 "$parked"
1303
+ merged="$(jq 'del(.model)' "$f")"
1304
+ printf '%s\n' "$merged" >"$f"
1305
+ chmod 600 "$f"
1306
+ gitignore_add ".weave-parked.json"
1307
+ ok "opencode is now ${C_BOLD}off${C_RESET} — pick a non-Weave model with /models. Takes effect on your next opencode run."
1308
+ ;;
1309
+ on)
1310
+ if [ "$on" = "true" ]; then ok "opencode is already on — nothing to do."; return 0; fi
1311
+ restore_model="weave/claude-sonnet-4-6"
1312
+ if [ "$parked_present" = "true" ]; then
1313
+ restore_model="$(jq -r '.model // "weave/claude-sonnet-4-6"' "$parked")"
1314
+ elif [ "$has_weave" != "true" ]; then
1315
+ warn "opencode isn't configured for the router. Run the installer first."; return 0
1316
+ else
1317
+ # No parked model (sidecar deleted by hand). Derive the default from the
1318
+ # installed provider.weave.models block rather than a hardcoded literal
1319
+ # that silently diverges when the installer's default changes — prefer a
1320
+ # sonnet entry, else the first model the installer registered.
1321
+ restore_model="$(jq -r '
1322
+ (.provider.weave.models // {} | keys) as $k
1323
+ | (([$k[] | select(test("sonnet"))] | first) // $k[0] // "claude-sonnet-4-6")
1324
+ | "weave/" + .
1325
+ ' "$f" 2>/dev/null || echo "weave/claude-sonnet-4-6")"
1326
+ fi
1327
+ merged="$(jq --arg m "$restore_model" '.model = $m' "$f")"
1328
+ printf '%s\n' "$merged" >"$f"
1329
+ chmod 600 "$f"
1330
+ rm -f "$parked"
1331
+ ok "opencode is now ${C_BOLD}on${C_RESET} (default model $restore_model via the Weave Router). Takes effect on your next opencode run."
1332
+ ;;
1333
+ esac
1334
+ }
1335
+
1336
+ if [ "$mode" != "install" ]; then
1337
+ case "$target" in
1338
+ claude) toggle_claude ;;
1339
+ codex) toggle_codex ;;
1340
+ opencode) toggle_opencode ;;
1341
+ esac
1342
+ exit 0
1343
+ fi
1344
+
938
1345
  # ---------- token handling ----------
939
1346
 
940
1347
  api_key=""
@@ -1027,16 +1434,44 @@ install_slash_commands() {
1027
1434
  refuse_if_symlink "$dst_dir"
1028
1435
  fi
1029
1436
  mkdir -p "$dst_dir"
1030
- for cmd in force-model unforce-model; do
1437
+
1438
+ # force-model/unforce-model are router-intercepted prompt expansions and
1439
+ # apply to every target. The router-off/on/status wrappers shell out to this
1440
+ # installer to flip the *local* config, so they're Claude Code-only and need
1441
+ # the install scope baked into the command (the .md can't discover it at
1442
+ # invocation time). {{SCOPE}} is substituted accordingly.
1443
+ installed="force-model, unforce-model"
1444
+ cmds="force-model unforce-model"
1445
+ if [ "$target" = "claude" ]; then
1446
+ cmds="$cmds router-off router-on router-status"
1447
+ installed="$installed, router-off, router-on, router-status"
1448
+ fi
1449
+
1450
+ # Bake the same scope selector the toggle needs to find this install: --dir
1451
+ # overrides scope (mirrors install/uninstall path resolution), so a sandbox
1452
+ # install gets `--dir <path>` and the slash commands flip that dir rather
1453
+ # than the default user-scope paths. printf %q quotes paths with spaces.
1454
+ local scope_args=""
1455
+ if [ -n "$install_dir" ]; then
1456
+ scope_args=" --dir $(printf '%q' "$install_dir")"
1457
+ elif [ "$scope" = "project" ]; then
1458
+ scope_args=" --scope project"
1459
+ fi
1460
+
1461
+ for cmd in $cmds; do
1031
1462
  src="$commands_src_dir/$cmd.md"
1032
1463
  dst="$dst_dir/$cmd.md"
1033
1464
  [ -f "$src" ] || continue
1034
1465
  if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
1035
1466
  refuse_if_symlink "$dst"
1036
1467
  fi
1037
- cp "$src" "$dst"
1468
+ # Substitute the {{SCOPE}} placeholder (only the router-* wrappers carry it;
1469
+ # cp-equivalent for the others since the token is absent).
1470
+ local body; body="$(cat "$src")"
1471
+ body="${body//\{\{SCOPE\}\}/$scope_args}"
1472
+ printf '%s\n' "$body" >"$dst"
1038
1473
  done
1039
- ok "Slash commands written to $dst_dir (force-model, unforce-model)"
1474
+ ok "Slash commands written to $dst_dir ($installed)"
1040
1475
  }
1041
1476
 
1042
1477
  # ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
@@ -1091,6 +1526,7 @@ if [ "$target" = "codex" ]; then
1091
1526
  # point CODEX_HOME at the directory we wrote so Codex finds our config.
1092
1527
  info "Run Codex with CODEX_HOME=$codex_dir codex so it picks up this config."
1093
1528
  fi
1529
+ print_uninstall_hint
1094
1530
  exit 0
1095
1531
  fi
1096
1532
 
@@ -1141,6 +1577,7 @@ if [ "$target" = "opencode" ]; then
1141
1577
  # has to point opencode at the file explicitly.
1142
1578
  info "Run opencode with OPENCODE_CONFIG=$opencode_config_file opencode."
1143
1579
  fi
1580
+ print_uninstall_hint
1144
1581
  exit 0
1145
1582
  fi
1146
1583
 
@@ -1279,8 +1716,9 @@ normalize_model() {
1279
1716
  prices='{
1280
1717
  "input": {
1281
1718
  "claude-haiku-4-5": 0.0008,
1282
- "claude-opus-4-6": 0.015,
1283
- "claude-opus-4-7": 0.015,
1719
+ "claude-opus-4-6": 0.005,
1720
+ "claude-opus-4-7": 0.005,
1721
+ "claude-opus-4-8": 0.005,
1284
1722
  "claude-sonnet-4-5": 0.003,
1285
1723
  "claude-sonnet-4-6": 0.003,
1286
1724
  "deepseek/deepseek-v4-flash": 0.00014,
@@ -1316,21 +1754,22 @@ prices='{
1316
1754
  "mistralai/mistral-small-2603": 0.0002,
1317
1755
  "moonshotai/kimi-k2.5": 0.0006,
1318
1756
  "moonshotai/kimi-k2.6": 0.00095,
1319
- "qwen/qwen3-235b-a22b-2507": 0.000071,
1757
+ "qwen/qwen3-235b-a22b-2507": 0.0002266,
1320
1758
  "qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
1321
1759
  "qwen/qwen3-coder": 0.0009,
1322
1760
  "qwen/qwen3-coder-next": 0.0005,
1323
1761
  "qwen/qwen3-next-80b-a3b-instruct": 0.00015,
1324
1762
  "qwen/qwen3.5-flash-02-23": 0.00005,
1325
1763
  "qwen/qwen3.6-35b-a3b": 0.00015,
1326
- "xiaomi/mimo-v2.5": 0.0004,
1327
1764
  "xiaomi/mimo-v2.5-pro": 0.001,
1328
- "z-ai/glm-5": 0.0006
1765
+ "z-ai/glm-5": 0.0006,
1766
+ "z-ai/glm-5.1": 0.00105
1329
1767
  },
1330
1768
  "output": {
1331
1769
  "claude-haiku-4-5": 0.004,
1332
- "claude-opus-4-6": 0.075,
1333
- "claude-opus-4-7": 0.075,
1770
+ "claude-opus-4-6": 0.025,
1771
+ "claude-opus-4-7": 0.025,
1772
+ "claude-opus-4-8": 0.025,
1334
1773
  "claude-sonnet-4-5": 0.015,
1335
1774
  "claude-sonnet-4-6": 0.015,
1336
1775
  "deepseek/deepseek-v4-flash": 0.00028,
@@ -1366,16 +1805,16 @@ prices='{
1366
1805
  "mistralai/mistral-small-2603": 0.0006,
1367
1806
  "moonshotai/kimi-k2.5": 0.003,
1368
1807
  "moonshotai/kimi-k2.6": 0.004,
1369
- "qwen/qwen3-235b-a22b-2507": 0.000463,
1808
+ "qwen/qwen3-235b-a22b-2507": 0.0009064,
1370
1809
  "qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
1371
1810
  "qwen/qwen3-coder": 0.0027,
1372
1811
  "qwen/qwen3-coder-next": 0.0012,
1373
1812
  "qwen/qwen3-next-80b-a3b-instruct": 0.0012,
1374
1813
  "qwen/qwen3.5-flash-02-23": 0.00015,
1375
1814
  "qwen/qwen3.6-35b-a3b": 0.00095,
1376
- "xiaomi/mimo-v2.5": 0.002,
1377
1815
  "xiaomi/mimo-v2.5-pro": 0.003,
1378
- "z-ai/glm-5": 0.00208
1816
+ "z-ai/glm-5": 0.00208,
1817
+ "z-ai/glm-5.1": 0.0035
1379
1818
  }
1380
1819
  }'
1381
1820
  # END_GENERATED_PRICES
@@ -1676,3 +2115,4 @@ fi
1676
2115
  printf "\n"
1677
2116
  printf "%s✓%s %s%sWeave Router installed for Claude Code.%s\n" \
1678
2117
  "$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
2118
+ print_uninstall_hint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "One-command installer that points Claude Code at the Weave Router.",
5
5
  "bin": {
6
6
  "weave-router": "bin.js"
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"