@workweave/router 0.1.5 → 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 +30 -6
- package/cc-statusline.sh +37 -12
- package/commands/router-off.md +10 -0
- package/commands/router-on.md +10 -0
- package/commands/router-status.md +10 -0
- package/install.sh +624 -40
- package/package.json +1 -1
- package/uninstall.sh +112 -9
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# @workweave/router
|
|
2
2
|
|
|
3
|
-
One command, anywhere, to point Claude Code or
|
|
3
|
+
One command, anywhere, to point Claude Code, Codex, or opencode at the Weave Router.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npx @workweave/router # interactive: pick Claude Code
|
|
6
|
+
npx @workweave/router # interactive: pick Claude Code / Codex / opencode, then scope
|
|
7
7
|
npx @workweave/router --claude # skip the picker, target Claude Code
|
|
8
8
|
npx @workweave/router --codex # skip the picker, target the OpenAI Codex CLI
|
|
9
|
-
npx @workweave/router --
|
|
9
|
+
npx @workweave/router --opencode # skip the picker, target opencode
|
|
10
|
+
npx @workweave/router --scope project # per-repo install, commit settings.json (or .codex/ / opencode.json)
|
|
10
11
|
npx @workweave/router --local # self-hosted via docker-compose (localhost:8080)
|
|
11
12
|
npx @workweave/router --base-url https://router.acme.internal
|
|
12
13
|
npx @workweave/router --non-interactive # reads $WEAVE_ROUTER_KEY, no prompts (defaults to claude)
|
|
@@ -18,11 +19,27 @@ Version-pin for reproducible setups:
|
|
|
18
19
|
npx @workweave/router@0.1.0 --claude --scope project
|
|
19
20
|
```
|
|
20
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
|
+
|
|
21
37
|
Uninstall:
|
|
22
38
|
|
|
23
39
|
```bash
|
|
24
40
|
npx @workweave/router --uninstall # Claude Code, user scope
|
|
25
41
|
npx @workweave/router --uninstall --codex # Codex, user scope
|
|
42
|
+
npx @workweave/router --uninstall --opencode # opencode, user scope
|
|
26
43
|
npx @workweave/router --uninstall --scope project # Claude Code, inside the repo
|
|
27
44
|
npx @workweave/router --uninstall --codex --scope project
|
|
28
45
|
```
|
|
@@ -35,7 +52,7 @@ Node ≥ 18 — no `curl | sh`, no Git clone, no PATH fiddling. Everything the
|
|
|
35
52
|
shell installer documents (targets, scopes, flags, environment variables)
|
|
36
53
|
works identically here.
|
|
37
54
|
|
|
38
|
-
|
|
55
|
+
Three install targets:
|
|
39
56
|
|
|
40
57
|
- **Claude Code** (default) — patches `~/.claude/settings.json` (or
|
|
41
58
|
`<repo>/.claude/settings.json` with `--scope project`) so `claude` routes
|
|
@@ -47,6 +64,13 @@ Two install targets:
|
|
|
47
64
|
through to api.openai.com. The block lives between begin/end markers so
|
|
48
65
|
re-running the installer rewrites it cleanly and `--uninstall --codex`
|
|
49
66
|
removes it without touching the rest of your config.
|
|
67
|
+
- **opencode** (`--opencode`) — merges a `provider.weave` entry (backed by
|
|
68
|
+
opencode's built-in `@ai-sdk/anthropic` provider) into
|
|
69
|
+
`~/.config/opencode/opencode.json` (or `<repo>/opencode.json` with
|
|
70
|
+
`--scope project`). The router speaks the Anthropic Messages API
|
|
71
|
+
natively, so opencode talks to it unmodified. Re-install rewrites only
|
|
72
|
+
the managed `provider.weave` block; `--uninstall --opencode` strips it
|
|
73
|
+
and leaves your other providers and settings alone.
|
|
50
74
|
|
|
51
75
|
See the [main installer docs](https://github.com/workweave/router/tree/main/install)
|
|
52
76
|
for the full reference.
|
|
@@ -55,8 +79,8 @@ for the full reference.
|
|
|
55
79
|
|
|
56
80
|
- Node ≥ 18 (ships with `npx`)
|
|
57
81
|
- `bash` on PATH (macOS / Linux native; Windows needs Git Bash or WSL)
|
|
58
|
-
- `jq` on PATH — used by the Claude Code status line script
|
|
59
|
-
for the Codex path.
|
|
82
|
+
- `jq` on PATH — used by the Claude Code status line script and the
|
|
83
|
+
opencode JSON merge. Not required for the Codex path.
|
|
60
84
|
|
|
61
85
|
## Why npx
|
|
62
86
|
|
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,
|
|
@@ -164,20 +165,25 @@ prices='{
|
|
|
164
165
|
"gpt-5.5-nano": 0.00015,
|
|
165
166
|
"gpt-5.5-pro": 0.03,
|
|
166
167
|
"minimax/minimax-m2.7": 0.0003,
|
|
168
|
+
"mistralai/mistral-small-2603": 0.0002,
|
|
167
169
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
168
170
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
169
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
171
|
+
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
172
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
173
|
+
"qwen/qwen3-coder": 0.0009,
|
|
170
174
|
"qwen/qwen3-coder-next": 0.0005,
|
|
171
175
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
176
|
+
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
172
177
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
173
|
-
"xiaomi/mimo-v2.5": 0.0004,
|
|
174
178
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
175
|
-
"z-ai/glm-5": 0.0006
|
|
179
|
+
"z-ai/glm-5": 0.0006,
|
|
180
|
+
"z-ai/glm-5.1": 0.00105
|
|
176
181
|
},
|
|
177
182
|
"output": {
|
|
178
183
|
"claude-haiku-4-5": 0.004,
|
|
179
|
-
"claude-opus-4-6": 0.
|
|
180
|
-
"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,
|
|
181
187
|
"claude-sonnet-4-5": 0.015,
|
|
182
188
|
"claude-sonnet-4-6": 0.015,
|
|
183
189
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
@@ -210,15 +216,19 @@ prices='{
|
|
|
210
216
|
"gpt-5.5-nano": 0.0006,
|
|
211
217
|
"gpt-5.5-pro": 0.12,
|
|
212
218
|
"minimax/minimax-m2.7": 0.0012,
|
|
219
|
+
"mistralai/mistral-small-2603": 0.0006,
|
|
213
220
|
"moonshotai/kimi-k2.5": 0.003,
|
|
214
221
|
"moonshotai/kimi-k2.6": 0.004,
|
|
215
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
222
|
+
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
223
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
224
|
+
"qwen/qwen3-coder": 0.0027,
|
|
216
225
|
"qwen/qwen3-coder-next": 0.0012,
|
|
217
226
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
227
|
+
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
218
228
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
219
|
-
"xiaomi/mimo-v2.5": 0.002,
|
|
220
229
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
221
|
-
"z-ai/glm-5": 0.00208
|
|
230
|
+
"z-ai/glm-5": 0.00208,
|
|
231
|
+
"z-ai/glm-5.1": 0.0035
|
|
222
232
|
}
|
|
223
233
|
}'
|
|
224
234
|
# END_GENERATED_PRICES
|
|
@@ -274,9 +284,24 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
274
284
|
# The marker regex tolerates the optional "(<provider>)" segment and a
|
|
275
285
|
# `[1m]` / `-YYYYMMDD` suffix on either model name so transcripts written
|
|
276
286
|
# against context-tiered or dated model ids still parse cleanly.
|
|
287
|
+
#
|
|
288
|
+
# Dedup note: CC writes one JSONL entry per *content block* in an
|
|
289
|
+
# assistant turn (text, text, tool_use → 3 entries), and every entry
|
|
290
|
+
# carries the same `message.usage`. Summing per-entry triple-counts the
|
|
291
|
+
# turn. We dedupe on (message.id, message.usage) before summing:
|
|
292
|
+
# * For native Anthropic upstreams message.id is unique per turn, so
|
|
293
|
+
# this collapses the content-block fan-out cleanly.
|
|
294
|
+
# * For non-Anthropic upstreams that round-trip through the router's
|
|
295
|
+
# translator, message.id can be a constant placeholder
|
|
296
|
+
# ("msg_translated"); usage still differs per turn (input_tokens
|
|
297
|
+
# grows), so the composite key keeps turns distinct. Two turns with
|
|
298
|
+
# byte-identical id AND usage would still collapse, but that's a
|
|
299
|
+
# genuine retry/duplicate we want to drop.
|
|
277
300
|
read -r session_savings tot_in tot_out tot_cache_read tot_cache_write < <(
|
|
278
|
-
jq -
|
|
279
|
-
select(.type=="assistant") |
|
|
301
|
+
jq -rs --argjson p "$prices" --arg requested "$requested_norm" '
|
|
302
|
+
[.[] | select(.type=="assistant")] |
|
|
303
|
+
unique_by([.message.id, .message.usage]) |
|
|
304
|
+
.[] |
|
|
280
305
|
.message as $m |
|
|
281
306
|
($m.model // "" | sub("\\[[^]]*\\]$"; "") | sub("-[0-9]{8}$"; "")) as $rm |
|
|
282
307
|
{
|
|
@@ -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
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# Weave Router installer for Claude Code and
|
|
3
|
+
# Weave Router installer for Claude Code, Codex, and opencode.
|
|
4
4
|
#
|
|
5
|
-
# Configures Claude Code (default)
|
|
6
|
-
# permanently route through the Weave Router.
|
|
7
|
-
# the router base URL, router auth header,
|
|
8
|
-
# Code's settings.json. For Codex it writes
|
|
9
|
-
# entry plus `model_provider = "weave"` into
|
|
10
|
-
# block delimited by markers
|
|
5
|
+
# Configures Claude Code (default), the OpenAI Codex CLI (`--codex`), or
|
|
6
|
+
# opencode (`--opencode`) to permanently route through the Weave Router.
|
|
7
|
+
# For Claude Code this writes the router base URL, router auth header,
|
|
8
|
+
# and a status line into Claude Code's settings.json. For Codex it writes
|
|
9
|
+
# a `model_providers.weave` entry plus `model_provider = "weave"` into
|
|
10
|
+
# ~/.codex/config.toml (managed block delimited by markers). For opencode
|
|
11
|
+
# it merges a `provider.weave` block (anthropic-compatible) into
|
|
12
|
+
# opencode.json — since the file is JSON, install/uninstall are structural
|
|
13
|
+
# (jq) rather than marker-delimited.
|
|
11
14
|
#
|
|
12
|
-
# Two scopes (apply to
|
|
15
|
+
# Two scopes (apply to all targets):
|
|
13
16
|
# - user (default): ~/.claude/settings.json + ~/.weave/cc-statusline.sh
|
|
14
17
|
# ~/.codex/config.toml (with --codex)
|
|
18
|
+
# ~/.config/opencode/opencode.json (with --opencode)
|
|
15
19
|
# - project: <repo>/.claude/settings.json + <repo>/.claude/cc-statusline.sh
|
|
16
20
|
# <repo>/.codex/config.toml (with --codex)
|
|
21
|
+
# <repo>/opencode.json (with --opencode)
|
|
17
22
|
#
|
|
18
23
|
# Or pass --dir to install into any directory:
|
|
19
24
|
# - dir: <dir>/.claude/settings.json + <dir>/.claude/cc-statusline.sh
|
|
20
25
|
# <dir>/.codex/config.toml (with --codex)
|
|
26
|
+
# <dir>/opencode.json (with --opencode)
|
|
21
27
|
#
|
|
22
28
|
# Usage:
|
|
23
|
-
# npx @workweave/router # interactive picker (Claude Code
|
|
29
|
+
# npx @workweave/router # interactive picker (Claude Code, Codex, opencode)
|
|
24
30
|
# npx @workweave/router --claude # skip the picker, target Claude Code
|
|
25
31
|
# npx @workweave/router --codex # skip the picker, target the OpenAI Codex CLI
|
|
32
|
+
# npx @workweave/router --opencode # skip the picker, target opencode
|
|
26
33
|
# npx @workweave/router --scope project # commit-with-team install
|
|
27
34
|
# npx @workweave/router --dir /tmp/my-sandbox # isolated throwaway install
|
|
28
35
|
# npx @workweave/router --local # local router on localhost:8080
|
|
@@ -30,6 +37,18 @@
|
|
|
30
37
|
# npx @workweave/router --non-interactive # require WEAVE_ROUTER_KEY env var (defaults target to claude)
|
|
31
38
|
# npx @workweave/router --quiet # suppress banner, ping check, and trailing tips
|
|
32
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.
|
|
33
52
|
|
|
34
53
|
set -euo pipefail
|
|
35
54
|
|
|
@@ -47,14 +66,22 @@ non_interactive="false"
|
|
|
47
66
|
quiet="false"
|
|
48
67
|
router_key_header="X-Weave-Router-Key"
|
|
49
68
|
# Target tool whose config we patch. "claude" (default) writes Claude Code
|
|
50
|
-
# settings.json; "codex" writes ~/.codex/config.toml
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
69
|
+
# settings.json; "codex" writes ~/.codex/config.toml; "opencode" merges a
|
|
70
|
+
# provider block into opencode.json. Each target carries its own
|
|
71
|
+
# credential-passthrough story in the router: Claude Code's logged-in
|
|
72
|
+
# Anthropic key flows through unchanged, Codex's `OPENAI_API_KEY` flows
|
|
73
|
+
# through via the same header path, and opencode talks to the router via
|
|
74
|
+
# its anthropic-compatible API surface. target_explicit tracks whether
|
|
75
|
+
# --claude / --codex / --opencode was passed so an interactive run can
|
|
76
|
+
# prompt for the choice.
|
|
55
77
|
target="claude"
|
|
56
78
|
target_explicit="false"
|
|
57
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
|
+
|
|
58
85
|
# ---------- helpers ----------
|
|
59
86
|
|
|
60
87
|
# Detect whether stdout is a real terminal that grokks ANSI escapes. Pipes,
|
|
@@ -88,6 +115,29 @@ info() { printf "%s==>%s %s\n" "$C_CYAN" "$C_RESET" "$*"; }
|
|
|
88
115
|
ok() { printf "%s✓%s %s\n" "$C_GREEN" "$C_RESET" "$*"; }
|
|
89
116
|
skip() { printf "%s⊙%s %s%s%s\n" "$C_DIM" "$C_RESET" "$C_DIM" "$*" "$C_RESET"; }
|
|
90
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
|
+
|
|
91
141
|
# ---------- banner ----------
|
|
92
142
|
#
|
|
93
143
|
# Print the WEAVE wordmark in brand orange. Skipped under --quiet or when
|
|
@@ -97,8 +147,9 @@ print_banner() {
|
|
|
97
147
|
[ "$tty_out" = "true" ] || return 0
|
|
98
148
|
local target_label
|
|
99
149
|
case "$target" in
|
|
100
|
-
codex)
|
|
101
|
-
|
|
150
|
+
codex) target_label="Codex installer" ;;
|
|
151
|
+
opencode) target_label="opencode installer" ;;
|
|
152
|
+
*) target_label="Claude Code installer" ;;
|
|
102
153
|
esac
|
|
103
154
|
printf '\n'
|
|
104
155
|
printf '%s ╦ ╦╔═╗╔═╗╦ ╦╔═╗%s\n' "$C_BRAND" "$C_RESET"
|
|
@@ -473,6 +524,93 @@ TOML
|
|
|
473
524
|
chmod 600 "$config_file"
|
|
474
525
|
}
|
|
475
526
|
|
|
527
|
+
# write_opencode_config merges a managed `provider.weave` entry into opencode's
|
|
528
|
+
# opencode.json (anthropic-compatible — the router speaks the Anthropic
|
|
529
|
+
# Messages API natively, so opencode's bundled @ai-sdk/anthropic provider
|
|
530
|
+
# works unmodified). Re-running rewrites the block in-place via jq; uninstall
|
|
531
|
+
# strips it the same way. We also set `model` at the top level so a fresh
|
|
532
|
+
# `opencode` invocation picks the router by default; if the user has set
|
|
533
|
+
# their own model already, we leave it alone.
|
|
534
|
+
#
|
|
535
|
+
# Usage: write_opencode_config <config_file_path> <base_url> <api_key> [user_email] [user_name]
|
|
536
|
+
write_opencode_config() {
|
|
537
|
+
local config_file="$1"
|
|
538
|
+
local block_url="$2"
|
|
539
|
+
local block_key="$3"
|
|
540
|
+
local block_email="${4:-}"
|
|
541
|
+
local block_name="${5:-}"
|
|
542
|
+
|
|
543
|
+
# Build the headers object piecewise so empty email/name vanish from the
|
|
544
|
+
# final JSON. opencode forwards the `headers` map verbatim to the upstream
|
|
545
|
+
# provider, so the router sees the same X-Weave-* triplet here that it
|
|
546
|
+
# would from Claude Code or Codex. The X-App tag lets router telemetry
|
|
547
|
+
# attribute traffic to opencode specifically.
|
|
548
|
+
local headers_json
|
|
549
|
+
headers_json="$(jq -n \
|
|
550
|
+
--arg key "$block_key" \
|
|
551
|
+
--arg email "$block_email" \
|
|
552
|
+
--arg name "$block_name" '
|
|
553
|
+
{"X-Weave-Router-Key": $key, "X-App": "opencode"}
|
|
554
|
+
| (if $email != "" then . + {"X-Weave-User-Email": $email} else . end)
|
|
555
|
+
| (if $name != "" then . + {"X-Weave-User-Name": $name } else . end)
|
|
556
|
+
')"
|
|
557
|
+
|
|
558
|
+
# Headline models we surface in opencode's picker. The router re-routes
|
|
559
|
+
# each request anyway, so this list is mostly UX — what shows up when the
|
|
560
|
+
# user runs /models inside opencode. Keep it short and Anthropic-shaped
|
|
561
|
+
# so the bundled @ai-sdk/anthropic provider can request them.
|
|
562
|
+
#
|
|
563
|
+
# apiKey is set to the router key as well as planted in headers. opencode's
|
|
564
|
+
# @ai-sdk/anthropic provider treats apiKey as required at config-parse time
|
|
565
|
+
# and otherwise falls back to ANTHROPIC_API_KEY from the environment;
|
|
566
|
+
# without this, a user who's never had an Anthropic key in their shell hits
|
|
567
|
+
# a startup error before the router ever sees a request. The router itself
|
|
568
|
+
# ignores the value (auth runs off X-Weave-Router-Key); apiKey here is just
|
|
569
|
+
# a placeholder that satisfies the SDK's "is auth configured" check.
|
|
570
|
+
local block
|
|
571
|
+
block="$(jq -n \
|
|
572
|
+
--arg url "$block_url/v1" \
|
|
573
|
+
--arg key "$block_key" \
|
|
574
|
+
--argjson headers "$headers_json" '
|
|
575
|
+
{
|
|
576
|
+
npm: "@ai-sdk/anthropic",
|
|
577
|
+
name: "Weave Router",
|
|
578
|
+
options: { apiKey: $key, baseURL: $url, headers: $headers },
|
|
579
|
+
models: {
|
|
580
|
+
"claude-opus-4-8": { name: "Claude Opus 4.8 (via Weave Router)" },
|
|
581
|
+
"claude-opus-4-7": { name: "Claude Opus 4.7 (via Weave Router)" },
|
|
582
|
+
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6 (via Weave Router)" },
|
|
583
|
+
"claude-haiku-4-5": { name: "Claude Haiku 4.5 (via Weave Router)" }
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
')"
|
|
587
|
+
|
|
588
|
+
# Merge into any existing opencode.json. We always overwrite provider.weave
|
|
589
|
+
# so re-install reflects the latest key/identity, but we leave the rest of
|
|
590
|
+
# the file (other providers, mcp, agent settings) untouched. Top-level
|
|
591
|
+
# `model` is only set when the user hasn't already picked one.
|
|
592
|
+
local merged
|
|
593
|
+
if [ -f "$config_file" ]; then
|
|
594
|
+
merged="$(jq --argjson block "$block" '
|
|
595
|
+
.provider = ((.provider // {}) | .weave = $block)
|
|
596
|
+
| (if (.model // "") == "" then .model = "weave/claude-sonnet-4-6" else . end)
|
|
597
|
+
| (.["$schema"] //= "https://opencode.ai/config.json")
|
|
598
|
+
' "$config_file")"
|
|
599
|
+
else
|
|
600
|
+
merged="$(jq -n --argjson block "$block" '
|
|
601
|
+
{
|
|
602
|
+
"$schema": "https://opencode.ai/config.json",
|
|
603
|
+
model: "weave/claude-sonnet-4-6",
|
|
604
|
+
provider: { weave: $block }
|
|
605
|
+
}
|
|
606
|
+
')"
|
|
607
|
+
fi
|
|
608
|
+
printf '%s\n' "$merged" >"$config_file"
|
|
609
|
+
# 0600: the file holds a router key. Even at user scope, mode 644 would
|
|
610
|
+
# leak the key to any local user on a shared box.
|
|
611
|
+
chmod 600 "$config_file"
|
|
612
|
+
}
|
|
613
|
+
|
|
476
614
|
# resolve_user_name mirrors resolve_user_email but for display name. Priority:
|
|
477
615
|
# WEAVE_USER_NAME env override → git config user.name → empty. We don't
|
|
478
616
|
# prompt for name independently: if email prompting yielded nothing, name
|
|
@@ -575,11 +713,20 @@ while [ $# -gt 0 ]; do
|
|
|
575
713
|
--codex)
|
|
576
714
|
target="codex"; target_explicit="true"; shift
|
|
577
715
|
;;
|
|
716
|
+
--opencode)
|
|
717
|
+
target="opencode"; target_explicit="true"; shift
|
|
718
|
+
;;
|
|
578
719
|
--claude)
|
|
579
|
-
# No-op selector for symmetry with --codex. Useful in
|
|
580
|
-
# want to skip the interactive picker without depending
|
|
720
|
+
# No-op selector for symmetry with --codex / --opencode. Useful in
|
|
721
|
+
# pipelines that want to skip the interactive picker without depending
|
|
722
|
+
# on the default.
|
|
581
723
|
target="claude"; target_explicit="true"; shift
|
|
582
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
|
+
;;
|
|
583
730
|
-h|--help)
|
|
584
731
|
usage 0
|
|
585
732
|
;;
|
|
@@ -589,6 +736,17 @@ while [ $# -gt 0 ]; do
|
|
|
589
736
|
esac
|
|
590
737
|
done
|
|
591
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
|
+
|
|
592
750
|
if [ -z "$base_url" ]; then
|
|
593
751
|
base_url="$DEFAULT_BASE_URL"
|
|
594
752
|
fi
|
|
@@ -607,11 +765,13 @@ if [ "$target_explicit" = "false" ] && [ "$non_interactive" = "false" ] && [ -r
|
|
|
607
765
|
printf "%sInstall target:%s\n" "$C_BOLD" "$C_RESET"
|
|
608
766
|
printf " %s1)%s Claude Code %s— patches ~/.claude/settings.json (or <repo>/.claude/)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
609
767
|
printf " %s2)%s Codex %s— patches ~/.codex/config.toml (or <repo>/.codex/)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
610
|
-
printf "
|
|
768
|
+
printf " %s3)%s opencode %s— patches ~/.config/opencode/opencode.json (or <repo>/opencode.json)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
769
|
+
printf "Choose %s[1/2/3]%s (default %s1%s): " "$C_BOLD" "$C_RESET" "$C_BOLD" "$C_RESET"
|
|
611
770
|
read -r target_choice </dev/tty || target_choice=""
|
|
612
771
|
case "${target_choice:-1}" in
|
|
613
772
|
1|""|claude|c|C) target="claude" ;;
|
|
614
773
|
2|codex|x|X) target="codex" ;;
|
|
774
|
+
3|opencode|o|O) target="opencode" ;;
|
|
615
775
|
*) err "invalid choice: $target_choice"; exit 2 ;;
|
|
616
776
|
esac
|
|
617
777
|
fi
|
|
@@ -620,7 +780,8 @@ fi
|
|
|
620
780
|
# users see when `make full-setup` hands off to install.sh is the wordmark,
|
|
621
781
|
# not a bare "Install scope:" line. Target prompt above already finalized
|
|
622
782
|
# $target, so the banner's per-target label reflects the user's choice.
|
|
623
|
-
|
|
783
|
+
# Toggle verbs stay terse — skip the banner so `status` prints one clean line.
|
|
784
|
+
[ "$mode" = "install" ] && print_banner
|
|
624
785
|
|
|
625
786
|
# ---------- interactive scope prompt ----------
|
|
626
787
|
|
|
@@ -635,6 +796,19 @@ if [ -z "$install_dir" ] && [ "$scope_explicit" = "false" ] && [ "$non_interacti
|
|
|
635
796
|
scope_project_path="<repo>/.codex/"
|
|
636
797
|
scope_cli_label="codex"
|
|
637
798
|
;;
|
|
799
|
+
opencode)
|
|
800
|
+
# Match the actual install path, which honors XDG_CONFIG_HOME. Showing a
|
|
801
|
+
# hardcoded "~/.config/opencode/" here lied to users with a custom
|
|
802
|
+
# $XDG_CONFIG_HOME — they'd see one path in the prompt and the installer
|
|
803
|
+
# would write to another.
|
|
804
|
+
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
|
|
805
|
+
scope_user_path="$XDG_CONFIG_HOME/opencode/"
|
|
806
|
+
else
|
|
807
|
+
scope_user_path="~/.config/opencode/"
|
|
808
|
+
fi
|
|
809
|
+
scope_project_path="<repo>/opencode.json"
|
|
810
|
+
scope_cli_label="opencode"
|
|
811
|
+
;;
|
|
638
812
|
*)
|
|
639
813
|
scope_user_path="~/.claude/"
|
|
640
814
|
scope_project_path="<repo>/.claude/"
|
|
@@ -675,14 +849,22 @@ fi
|
|
|
675
849
|
|
|
676
850
|
# ---------- pre-flight ----------
|
|
677
851
|
|
|
678
|
-
[ "$
|
|
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
|
|
679
857
|
|
|
680
858
|
# Codex install only writes a TOML file (managed via awk) so jq isn't needed.
|
|
681
|
-
# Claude Code's settings.json
|
|
682
|
-
|
|
859
|
+
# Claude Code's settings.json and opencode's opencode.json patching both use
|
|
860
|
+
# jq to deep-merge / structurally rewrite JSON. Toggling those clients reads
|
|
861
|
+
# and rewrites the same JSON, so jq is required there too.
|
|
862
|
+
if [ "$target" = "claude" ] || [ "$target" = "opencode" ]; then
|
|
683
863
|
require_cmd jq "macOS: 'brew install jq' · Debian/Ubuntu: 'sudo apt install jq'"
|
|
684
864
|
fi
|
|
685
|
-
|
|
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"
|
|
686
868
|
|
|
687
869
|
case "$target" in
|
|
688
870
|
claude)
|
|
@@ -697,6 +879,12 @@ case "$target" in
|
|
|
697
879
|
warn "Continuing — config.toml will be written and will take effect once Codex is installed."
|
|
698
880
|
fi
|
|
699
881
|
;;
|
|
882
|
+
opencode)
|
|
883
|
+
if ! command -v opencode >/dev/null 2>&1; then
|
|
884
|
+
warn "'opencode' not found on PATH. Install from https://opencode.ai (or 'npm install -g opencode-ai'), then re-run this script."
|
|
885
|
+
warn "Continuing — opencode.json will be written and will take effect once opencode is installed."
|
|
886
|
+
fi
|
|
887
|
+
;;
|
|
700
888
|
esac
|
|
701
889
|
|
|
702
890
|
script_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
@@ -767,7 +955,7 @@ if [ "$target" = "claude" ]; then
|
|
|
767
955
|
fi
|
|
768
956
|
|
|
769
957
|
mkdir -p "$settings_dir" "$statusline_dir"
|
|
770
|
-
|
|
958
|
+
elif [ "$target" = "codex" ]; then
|
|
771
959
|
# Codex CLI reads config from ~/.codex/config.toml by default. For project
|
|
772
960
|
# scope we write to <repo>/.codex/config.toml; the user invokes Codex with
|
|
773
961
|
# `CODEX_HOME=<repo>/.codex codex` (or runs from the repo if Codex auto-
|
|
@@ -782,6 +970,296 @@ else
|
|
|
782
970
|
fi
|
|
783
971
|
|
|
784
972
|
mkdir -p "$codex_dir"
|
|
973
|
+
else
|
|
974
|
+
# opencode discovers config in this order: $XDG_CONFIG_HOME/opencode/opencode.json
|
|
975
|
+
# (or ~/.config/opencode/opencode.json) for user scope, and opencode.json /
|
|
976
|
+
# opencode.jsonc walked up from CWD for project scope. We standardize on
|
|
977
|
+
# opencode.json at the repo root for project scope (the option teammates can
|
|
978
|
+
# commit) and the XDG path for user scope. The router key is embedded so
|
|
979
|
+
# opencode.json goes in .gitignore for project scope, same as Codex.
|
|
980
|
+
case "$scope" in
|
|
981
|
+
user)
|
|
982
|
+
opencode_dir="${XDG_CONFIG_HOME:-$settings_base/.config}/opencode"
|
|
983
|
+
;;
|
|
984
|
+
project)
|
|
985
|
+
opencode_dir="$settings_base"
|
|
986
|
+
;;
|
|
987
|
+
esac
|
|
988
|
+
# --dir overrides both scopes: drop opencode.json straight into <dir>/ so
|
|
989
|
+
# the sandbox is self-contained (mirrors how --dir behaves for Codex).
|
|
990
|
+
if [ -n "$install_dir" ]; then
|
|
991
|
+
opencode_dir="$install_dir"
|
|
992
|
+
fi
|
|
993
|
+
opencode_config_file="$opencode_dir/opencode.json"
|
|
994
|
+
|
|
995
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
996
|
+
refuse_if_symlink "$opencode_dir"
|
|
997
|
+
refuse_if_symlink "$opencode_config_file"
|
|
998
|
+
fi
|
|
999
|
+
|
|
1000
|
+
mkdir -p "$opencode_dir"
|
|
1001
|
+
fi
|
|
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
|
|
785
1263
|
fi
|
|
786
1264
|
|
|
787
1265
|
# ---------- token handling ----------
|
|
@@ -876,16 +1354,44 @@ install_slash_commands() {
|
|
|
876
1354
|
refuse_if_symlink "$dst_dir"
|
|
877
1355
|
fi
|
|
878
1356
|
mkdir -p "$dst_dir"
|
|
879
|
-
|
|
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
|
|
880
1382
|
src="$commands_src_dir/$cmd.md"
|
|
881
1383
|
dst="$dst_dir/$cmd.md"
|
|
882
1384
|
[ -f "$src" ] || continue
|
|
883
1385
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
884
1386
|
refuse_if_symlink "$dst"
|
|
885
1387
|
fi
|
|
886
|
-
|
|
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"
|
|
887
1393
|
done
|
|
888
|
-
ok "Slash commands written to $dst_dir (
|
|
1394
|
+
ok "Slash commands written to $dst_dir ($installed)"
|
|
889
1395
|
}
|
|
890
1396
|
|
|
891
1397
|
# ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
|
|
@@ -940,6 +1446,58 @@ if [ "$target" = "codex" ]; then
|
|
|
940
1446
|
# point CODEX_HOME at the directory we wrote so Codex finds our config.
|
|
941
1447
|
info "Run Codex with CODEX_HOME=$codex_dir codex so it picks up this config."
|
|
942
1448
|
fi
|
|
1449
|
+
print_uninstall_hint
|
|
1450
|
+
exit 0
|
|
1451
|
+
fi
|
|
1452
|
+
|
|
1453
|
+
# ---------- opencode install path (dispatch + exit before the Claude-only writes) ----------
|
|
1454
|
+
|
|
1455
|
+
if [ "$target" = "opencode" ]; then
|
|
1456
|
+
write_opencode_config "$opencode_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
1457
|
+
ok "opencode config written to $opencode_config_file"
|
|
1458
|
+
|
|
1459
|
+
# Project scope: the per-teammate config carries the router key, so it
|
|
1460
|
+
# stays out of git. Same reasoning as the Codex path — base URL is shared,
|
|
1461
|
+
# but the key is per-person.
|
|
1462
|
+
if [ "$scope" = "project" ] && [ -z "$install_dir" ] && [ -n "${git_root:-}" ]; then
|
|
1463
|
+
gitignore="$git_root/.gitignore"
|
|
1464
|
+
refuse_if_symlink "$gitignore"
|
|
1465
|
+
for entry in \
|
|
1466
|
+
"opencode.json"
|
|
1467
|
+
do
|
|
1468
|
+
if [ ! -f "$gitignore" ] || ! grep -qxF "$entry" "$gitignore"; then
|
|
1469
|
+
printf '%s\n' "$entry" >>"$gitignore"
|
|
1470
|
+
fi
|
|
1471
|
+
done
|
|
1472
|
+
ok "Updated $gitignore (ignored opencode.json)"
|
|
1473
|
+
fi
|
|
1474
|
+
|
|
1475
|
+
# Post-install verification: same probes the Claude/Codex paths run.
|
|
1476
|
+
if [ "$quiet" != "true" ]; then
|
|
1477
|
+
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
1478
|
+
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
1479
|
+
fi
|
|
1480
|
+
fi
|
|
1481
|
+
|
|
1482
|
+
if [ -n "$api_key" ]; then
|
|
1483
|
+
validate_opencode_key() {
|
|
1484
|
+
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
1485
|
+
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
1486
|
+
}
|
|
1487
|
+
if ! spin "Validating API key" validate_opencode_key; then
|
|
1488
|
+
warn "Router rejected the API key (check it matches the dashboard at $base_url/ui/)."
|
|
1489
|
+
fi
|
|
1490
|
+
fi
|
|
1491
|
+
|
|
1492
|
+
printf "\n"
|
|
1493
|
+
printf "%s✓%s %s%sWeave Router installed for opencode.%s\n" \
|
|
1494
|
+
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
1495
|
+
if [ -n "$install_dir" ]; then
|
|
1496
|
+
# --dir installs land outside opencode's discovery roots, so the caller
|
|
1497
|
+
# has to point opencode at the file explicitly.
|
|
1498
|
+
info "Run opencode with OPENCODE_CONFIG=$opencode_config_file opencode."
|
|
1499
|
+
fi
|
|
1500
|
+
print_uninstall_hint
|
|
943
1501
|
exit 0
|
|
944
1502
|
fi
|
|
945
1503
|
|
|
@@ -1078,8 +1636,9 @@ normalize_model() {
|
|
|
1078
1636
|
prices='{
|
|
1079
1637
|
"input": {
|
|
1080
1638
|
"claude-haiku-4-5": 0.0008,
|
|
1081
|
-
"claude-opus-4-6": 0.
|
|
1082
|
-
"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,
|
|
1083
1642
|
"claude-sonnet-4-5": 0.003,
|
|
1084
1643
|
"claude-sonnet-4-6": 0.003,
|
|
1085
1644
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
@@ -1112,20 +1671,25 @@ prices='{
|
|
|
1112
1671
|
"gpt-5.5-nano": 0.00015,
|
|
1113
1672
|
"gpt-5.5-pro": 0.03,
|
|
1114
1673
|
"minimax/minimax-m2.7": 0.0003,
|
|
1674
|
+
"mistralai/mistral-small-2603": 0.0002,
|
|
1115
1675
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
1116
1676
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
1117
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
1677
|
+
"qwen/qwen3-235b-a22b-2507": 0.0002266,
|
|
1678
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
1679
|
+
"qwen/qwen3-coder": 0.0009,
|
|
1118
1680
|
"qwen/qwen3-coder-next": 0.0005,
|
|
1119
1681
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
1682
|
+
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
1120
1683
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
1121
|
-
"xiaomi/mimo-v2.5": 0.0004,
|
|
1122
1684
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
1123
|
-
"z-ai/glm-5": 0.0006
|
|
1685
|
+
"z-ai/glm-5": 0.0006,
|
|
1686
|
+
"z-ai/glm-5.1": 0.00105
|
|
1124
1687
|
},
|
|
1125
1688
|
"output": {
|
|
1126
1689
|
"claude-haiku-4-5": 0.004,
|
|
1127
|
-
"claude-opus-4-6": 0.
|
|
1128
|
-
"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,
|
|
1129
1693
|
"claude-sonnet-4-5": 0.015,
|
|
1130
1694
|
"claude-sonnet-4-6": 0.015,
|
|
1131
1695
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
@@ -1158,15 +1722,19 @@ prices='{
|
|
|
1158
1722
|
"gpt-5.5-nano": 0.0006,
|
|
1159
1723
|
"gpt-5.5-pro": 0.12,
|
|
1160
1724
|
"minimax/minimax-m2.7": 0.0012,
|
|
1725
|
+
"mistralai/mistral-small-2603": 0.0006,
|
|
1161
1726
|
"moonshotai/kimi-k2.5": 0.003,
|
|
1162
1727
|
"moonshotai/kimi-k2.6": 0.004,
|
|
1163
|
-
"qwen/qwen3-235b-a22b-2507": 0.
|
|
1728
|
+
"qwen/qwen3-235b-a22b-2507": 0.0009064,
|
|
1729
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
1730
|
+
"qwen/qwen3-coder": 0.0027,
|
|
1164
1731
|
"qwen/qwen3-coder-next": 0.0012,
|
|
1165
1732
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
1733
|
+
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
1166
1734
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
1167
|
-
"xiaomi/mimo-v2.5": 0.002,
|
|
1168
1735
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
1169
|
-
"z-ai/glm-5": 0.00208
|
|
1736
|
+
"z-ai/glm-5": 0.00208,
|
|
1737
|
+
"z-ai/glm-5.1": 0.0035
|
|
1170
1738
|
}
|
|
1171
1739
|
}'
|
|
1172
1740
|
# END_GENERATED_PRICES
|
|
@@ -1222,9 +1790,24 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
1222
1790
|
# The marker regex tolerates the optional "(<provider>)" segment and a
|
|
1223
1791
|
# `[1m]` / `-YYYYMMDD` suffix on either model name so transcripts written
|
|
1224
1792
|
# against context-tiered or dated model ids still parse cleanly.
|
|
1793
|
+
#
|
|
1794
|
+
# Dedup note: CC writes one JSONL entry per *content block* in an
|
|
1795
|
+
# assistant turn (text, text, tool_use → 3 entries), and every entry
|
|
1796
|
+
# carries the same `message.usage`. Summing per-entry triple-counts the
|
|
1797
|
+
# turn. We dedupe on (message.id, message.usage) before summing:
|
|
1798
|
+
# * For native Anthropic upstreams message.id is unique per turn, so
|
|
1799
|
+
# this collapses the content-block fan-out cleanly.
|
|
1800
|
+
# * For non-Anthropic upstreams that round-trip through the router's
|
|
1801
|
+
# translator, message.id can be a constant placeholder
|
|
1802
|
+
# ("msg_translated"); usage still differs per turn (input_tokens
|
|
1803
|
+
# grows), so the composite key keeps turns distinct. Two turns with
|
|
1804
|
+
# byte-identical id AND usage would still collapse, but that's a
|
|
1805
|
+
# genuine retry/duplicate we want to drop.
|
|
1225
1806
|
read -r session_savings tot_in tot_out tot_cache_read tot_cache_write < <(
|
|
1226
|
-
jq -
|
|
1227
|
-
select(.type=="assistant") |
|
|
1807
|
+
jq -rs --argjson p "$prices" --arg requested "$requested_norm" '
|
|
1808
|
+
[.[] | select(.type=="assistant")] |
|
|
1809
|
+
unique_by([.message.id, .message.usage]) |
|
|
1810
|
+
.[] |
|
|
1228
1811
|
.message as $m |
|
|
1229
1812
|
($m.model // "" | sub("\\[[^]]*\\]$"; "") | sub("-[0-9]{8}$"; "")) as $rm |
|
|
1230
1813
|
{
|
|
@@ -1452,3 +2035,4 @@ fi
|
|
|
1452
2035
|
printf "\n"
|
|
1453
2036
|
printf "%s✓%s %s%sWeave Router installed for Claude Code.%s\n" \
|
|
1454
2037
|
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
2038
|
+
print_uninstall_hint
|
package/package.json
CHANGED
package/uninstall.sh
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# Weave Router uninstaller for Claude Code and
|
|
3
|
+
# Weave Router uninstaller for Claude Code, Codex, and opencode.
|
|
4
4
|
#
|
|
5
5
|
# Default target is Claude Code: removes the env vars, statusLine, and local
|
|
6
6
|
# router auth that install.sh added; leaves the rest of settings.json
|
|
7
7
|
# untouched. Pass --codex to strip the managed [model_providers.weave]
|
|
8
8
|
# block (and the matching top-level `model_provider`) from Codex's
|
|
9
|
-
# config.toml — anything outside the markers is preserved.
|
|
9
|
+
# config.toml — anything outside the markers is preserved. Pass --opencode
|
|
10
|
+
# to strip the `provider.weave` block (and the top-level `model` key when
|
|
11
|
+
# it points at the router) from opencode.json; other providers and user
|
|
12
|
+
# settings are preserved.
|
|
10
13
|
#
|
|
11
14
|
# Usage:
|
|
12
15
|
# npx @workweave/router --uninstall # Claude Code, user scope
|
|
13
16
|
# npx @workweave/router --uninstall --codex # Codex, user scope
|
|
17
|
+
# npx @workweave/router --uninstall --opencode # opencode, user scope
|
|
14
18
|
# npx @workweave/router --uninstall --scope project # run inside the repo
|
|
15
19
|
# npx @workweave/router --uninstall --dir /tmp/test # --dir alone (user scope, .weave/)
|
|
16
20
|
# npx @workweave/router --uninstall --scope project --dir /tmp # --dir + project scope (.claude/)
|
|
@@ -52,10 +56,14 @@ while [ $# -gt 0 ]; do
|
|
|
52
56
|
--codex)
|
|
53
57
|
target="codex"; shift
|
|
54
58
|
;;
|
|
59
|
+
--opencode)
|
|
60
|
+
target="opencode"; shift
|
|
61
|
+
;;
|
|
55
62
|
--claude)
|
|
56
|
-
# No-op selector for symmetry with --codex and install.sh's
|
|
57
|
-
# Lets `./install.sh --uninstall --claude` (which forwards
|
|
58
|
-
# args here) succeed instead of hitting the unknown-flag
|
|
63
|
+
# No-op selector for symmetry with --codex / --opencode and install.sh's
|
|
64
|
+
# --claude. Lets `./install.sh --uninstall --claude` (which forwards
|
|
65
|
+
# remaining args here) succeed instead of hitting the unknown-flag
|
|
66
|
+
# catch-all.
|
|
59
67
|
target="claude"; shift
|
|
60
68
|
;;
|
|
61
69
|
-h|--help)
|
|
@@ -69,8 +77,8 @@ while [ $# -gt 0 ]; do
|
|
|
69
77
|
esac
|
|
70
78
|
done
|
|
71
79
|
|
|
72
|
-
if [ "$target" = "claude" ] && ! command -v jq >/dev/null 2>&1; then
|
|
73
|
-
err "jq is required for the
|
|
80
|
+
if { [ "$target" = "claude" ] || [ "$target" = "opencode" ]; } && ! command -v jq >/dev/null 2>&1; then
|
|
81
|
+
err "jq is required for the $target uninstall path."
|
|
74
82
|
exit 1
|
|
75
83
|
fi
|
|
76
84
|
|
|
@@ -97,6 +105,90 @@ strip_codex_block() {
|
|
|
97
105
|
mv "$tmp" "$config_file"
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
# ---------- opencode uninstall path ----------
|
|
109
|
+
|
|
110
|
+
if [ "$target" = "opencode" ]; then
|
|
111
|
+
# Resolve opencode_config_file based on scope/dir. Mirrors install.sh so
|
|
112
|
+
# an `install --opencode --scope project` is exactly reversed by an
|
|
113
|
+
# `uninstall --opencode --scope project`.
|
|
114
|
+
if [ -n "$install_dir" ]; then
|
|
115
|
+
install_dir="$(cd "$install_dir" 2>/dev/null && pwd || echo "$install_dir")"
|
|
116
|
+
opencode_dir="$install_dir"
|
|
117
|
+
refuse_if_symlink "$opencode_dir"
|
|
118
|
+
elif [ "$scope" = "user" ]; then
|
|
119
|
+
opencode_dir="${XDG_CONFIG_HOME:-$HOME/.config}/opencode"
|
|
120
|
+
else
|
|
121
|
+
# Project scope: same prompt + git-root fallback as install.sh.
|
|
122
|
+
project_dir=""
|
|
123
|
+
if [ "$scope_explicit" = "false" ] && [ -r /dev/tty ]; then
|
|
124
|
+
default_project_dir="$(pwd)"
|
|
125
|
+
printf "Project directory to uninstall from [default: %s]: " "$default_project_dir"
|
|
126
|
+
read -r project_dir_choice </dev/tty || project_dir_choice=""
|
|
127
|
+
project_dir="${project_dir_choice:-$default_project_dir}"
|
|
128
|
+
case "$project_dir" in
|
|
129
|
+
"~") project_dir="$HOME" ;;
|
|
130
|
+
"~/"*) project_dir="$HOME/${project_dir#~/}" ;;
|
|
131
|
+
esac
|
|
132
|
+
if [ ! -d "$project_dir" ]; then
|
|
133
|
+
err "directory does not exist: $project_dir"
|
|
134
|
+
exit 1
|
|
135
|
+
fi
|
|
136
|
+
project_dir="$(cd "$project_dir" && pwd)"
|
|
137
|
+
fi
|
|
138
|
+
if [ -n "${project_dir:-}" ]; then
|
|
139
|
+
opencode_dir="$project_dir"
|
|
140
|
+
else
|
|
141
|
+
if ! git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
|
|
142
|
+
err "--scope project must be run inside a git repo, or use --dir <path>."
|
|
143
|
+
exit 1
|
|
144
|
+
fi
|
|
145
|
+
opencode_dir="$git_root"
|
|
146
|
+
fi
|
|
147
|
+
refuse_if_symlink "$opencode_dir"
|
|
148
|
+
fi
|
|
149
|
+
opencode_config_file="$opencode_dir/opencode.json"
|
|
150
|
+
refuse_if_symlink "$opencode_config_file"
|
|
151
|
+
|
|
152
|
+
if [ -f "$opencode_config_file" ]; then
|
|
153
|
+
# Strip `provider.weave` and any router-pointing top-level model. Other
|
|
154
|
+
# providers, user-set models that don't reference the router, and any
|
|
155
|
+
# unrelated keys are preserved.
|
|
156
|
+
cleaned="$(jq '
|
|
157
|
+
(if .provider.weave then del(.provider.weave) else . end)
|
|
158
|
+
| (if (.provider // {}) == {} then del(.provider) else . end)
|
|
159
|
+
| (if (.model // "" | tostring | startswith("weave/")) then del(.model) else . end)
|
|
160
|
+
' "$opencode_config_file")"
|
|
161
|
+
printf '%s\n' "$cleaned" >"$opencode_config_file"
|
|
162
|
+
|
|
163
|
+
# If only the $schema marker remains (or the file is empty), drop the
|
|
164
|
+
# file entirely so we don't leave a one-key artifact.
|
|
165
|
+
remaining_keys="$(jq -r 'del(."$schema") | keys | length' "$opencode_config_file" 2>/dev/null || echo 0)"
|
|
166
|
+
if [ "$remaining_keys" = "0" ]; then
|
|
167
|
+
rm -f "$opencode_config_file"
|
|
168
|
+
ok "Removed empty $opencode_config_file"
|
|
169
|
+
else
|
|
170
|
+
ok "Cleaned $opencode_config_file"
|
|
171
|
+
fi
|
|
172
|
+
else
|
|
173
|
+
info "No opencode config at $opencode_config_file (already uninstalled?)"
|
|
174
|
+
fi
|
|
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
|
+
|
|
184
|
+
if [ -n "$install_dir" ]; then
|
|
185
|
+
ok "Weave Router uninstalled from $install_dir (opencode)."
|
|
186
|
+
else
|
|
187
|
+
ok "Weave Router uninstalled (opencode, scope=$scope)."
|
|
188
|
+
fi
|
|
189
|
+
exit 0
|
|
190
|
+
fi
|
|
191
|
+
|
|
100
192
|
# ---------- codex uninstall path ----------
|
|
101
193
|
|
|
102
194
|
if [ "$target" = "codex" ]; then
|
|
@@ -267,9 +359,12 @@ else
|
|
|
267
359
|
fi
|
|
268
360
|
|
|
269
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.
|
|
270
365
|
cleaned="$(jq '
|
|
271
366
|
if .env then
|
|
272
|
-
.env |= (del(.ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS))
|
|
367
|
+
.env |= (del(.ANTHROPIC_BASE_URL, .ANTHROPIC_AUTH_TOKEN, .ANTHROPIC_CUSTOM_HEADERS))
|
|
273
368
|
| (if (.env | length) == 0 then del(.env) else . end)
|
|
274
369
|
else . end
|
|
275
370
|
| del(.apiKeyHelper)
|
|
@@ -278,6 +373,14 @@ if [ -n "$local_settings_file" ] && [ -f "$local_settings_file" ]; then
|
|
|
278
373
|
ok "Cleaned $local_settings_file"
|
|
279
374
|
fi
|
|
280
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
|
+
|
|
281
384
|
for f in "$statusline_file"; do
|
|
282
385
|
if [ -f "$f" ]; then
|
|
283
386
|
rm -f "$f"
|
|
@@ -291,7 +394,7 @@ done
|
|
|
291
394
|
commands_dir="$(dirname "$settings_file")/commands"
|
|
292
395
|
if [ -d "$commands_dir" ]; then
|
|
293
396
|
refuse_if_symlink "$commands_dir"
|
|
294
|
-
for cmd in force-model unforce-model; do
|
|
397
|
+
for cmd in force-model unforce-model router-off router-on router-status; do
|
|
295
398
|
cmd_file="$commands_dir/$cmd.md"
|
|
296
399
|
if [ -f "$cmd_file" ]; then
|
|
297
400
|
refuse_if_symlink "$cmd_file"
|