@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 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,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
- for cmd in force-model unforce-model; do
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
- cp "$src" "$dst"
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 (force-model, unforce-model)"
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.015,
1283
- "claude-opus-4-7": 0.015,
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.000071,
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.075,
1333
- "claude-opus-4-7": 0.075,
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.000463,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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"