@workweave/router 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/cc-statusline.sh +27 -2
- package/commands/force-model.md +6 -0
- package/commands/unforce-model.md +5 -0
- package/install.sh +304 -24
- package/package.json +2 -1
- package/uninstall.sh +125 -7
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)
|
|
@@ -23,6 +24,7 @@ Uninstall:
|
|
|
23
24
|
```bash
|
|
24
25
|
npx @workweave/router --uninstall # Claude Code, user scope
|
|
25
26
|
npx @workweave/router --uninstall --codex # Codex, user scope
|
|
27
|
+
npx @workweave/router --uninstall --opencode # opencode, user scope
|
|
26
28
|
npx @workweave/router --uninstall --scope project # Claude Code, inside the repo
|
|
27
29
|
npx @workweave/router --uninstall --codex --scope project
|
|
28
30
|
```
|
|
@@ -35,7 +37,7 @@ Node ≥ 18 — no `curl | sh`, no Git clone, no PATH fiddling. Everything the
|
|
|
35
37
|
shell installer documents (targets, scopes, flags, environment variables)
|
|
36
38
|
works identically here.
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
Three install targets:
|
|
39
41
|
|
|
40
42
|
- **Claude Code** (default) — patches `~/.claude/settings.json` (or
|
|
41
43
|
`<repo>/.claude/settings.json` with `--scope project`) so `claude` routes
|
|
@@ -47,6 +49,13 @@ Two install targets:
|
|
|
47
49
|
through to api.openai.com. The block lives between begin/end markers so
|
|
48
50
|
re-running the installer rewrites it cleanly and `--uninstall --codex`
|
|
49
51
|
removes it without touching the rest of your config.
|
|
52
|
+
- **opencode** (`--opencode`) — merges a `provider.weave` entry (backed by
|
|
53
|
+
opencode's built-in `@ai-sdk/anthropic` provider) into
|
|
54
|
+
`~/.config/opencode/opencode.json` (or `<repo>/opencode.json` with
|
|
55
|
+
`--scope project`). The router speaks the Anthropic Messages API
|
|
56
|
+
natively, so opencode talks to it unmodified. Re-install rewrites only
|
|
57
|
+
the managed `provider.weave` block; `--uninstall --opencode` strips it
|
|
58
|
+
and leaves your other providers and settings alone.
|
|
50
59
|
|
|
51
60
|
See the [main installer docs](https://github.com/workweave/router/tree/main/install)
|
|
52
61
|
for the full reference.
|
|
@@ -55,8 +64,8 @@ for the full reference.
|
|
|
55
64
|
|
|
56
65
|
- Node ≥ 18 (ships with `npx`)
|
|
57
66
|
- `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.
|
|
67
|
+
- `jq` on PATH — used by the Claude Code status line script and the
|
|
68
|
+
opencode JSON merge. Not required for the Codex path.
|
|
60
69
|
|
|
61
70
|
## Why npx
|
|
62
71
|
|
package/cc-statusline.sh
CHANGED
|
@@ -145,6 +145,7 @@ prices='{
|
|
|
145
145
|
"gemini-3-pro-preview": 0.002,
|
|
146
146
|
"gemini-3.1-flash-lite-preview": 0.0001,
|
|
147
147
|
"gemini-3.1-pro-preview": 0.002,
|
|
148
|
+
"gemini-3.5-flash": 0.0015,
|
|
148
149
|
"gpt-4.1": 0.002,
|
|
149
150
|
"gpt-4.1-mini": 0.0004,
|
|
150
151
|
"gpt-4.1-nano": 0.0001,
|
|
@@ -163,11 +164,15 @@ prices='{
|
|
|
163
164
|
"gpt-5.5-nano": 0.00015,
|
|
164
165
|
"gpt-5.5-pro": 0.03,
|
|
165
166
|
"minimax/minimax-m2.7": 0.0003,
|
|
167
|
+
"mistralai/mistral-small-2603": 0.0002,
|
|
166
168
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
167
169
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
168
170
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
171
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
172
|
+
"qwen/qwen3-coder": 0.0009,
|
|
169
173
|
"qwen/qwen3-coder-next": 0.0005,
|
|
170
174
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
175
|
+
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
171
176
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
172
177
|
"xiaomi/mimo-v2.5": 0.0004,
|
|
173
178
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
@@ -190,6 +195,7 @@ prices='{
|
|
|
190
195
|
"gemini-3-pro-preview": 0.008,
|
|
191
196
|
"gemini-3.1-flash-lite-preview": 0.0004,
|
|
192
197
|
"gemini-3.1-pro-preview": 0.008,
|
|
198
|
+
"gemini-3.5-flash": 0.009,
|
|
193
199
|
"gpt-4.1": 0.008,
|
|
194
200
|
"gpt-4.1-mini": 0.0016,
|
|
195
201
|
"gpt-4.1-nano": 0.0004,
|
|
@@ -208,11 +214,15 @@ prices='{
|
|
|
208
214
|
"gpt-5.5-nano": 0.0006,
|
|
209
215
|
"gpt-5.5-pro": 0.12,
|
|
210
216
|
"minimax/minimax-m2.7": 0.0012,
|
|
217
|
+
"mistralai/mistral-small-2603": 0.0006,
|
|
211
218
|
"moonshotai/kimi-k2.5": 0.003,
|
|
212
219
|
"moonshotai/kimi-k2.6": 0.004,
|
|
213
220
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
221
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
222
|
+
"qwen/qwen3-coder": 0.0027,
|
|
214
223
|
"qwen/qwen3-coder-next": 0.0012,
|
|
215
224
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
225
|
+
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
216
226
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
217
227
|
"xiaomi/mimo-v2.5": 0.002,
|
|
218
228
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
@@ -272,9 +282,24 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
272
282
|
# The marker regex tolerates the optional "(<provider>)" segment and a
|
|
273
283
|
# `[1m]` / `-YYYYMMDD` suffix on either model name so transcripts written
|
|
274
284
|
# against context-tiered or dated model ids still parse cleanly.
|
|
285
|
+
#
|
|
286
|
+
# Dedup note: CC writes one JSONL entry per *content block* in an
|
|
287
|
+
# assistant turn (text, text, tool_use → 3 entries), and every entry
|
|
288
|
+
# carries the same `message.usage`. Summing per-entry triple-counts the
|
|
289
|
+
# turn. We dedupe on (message.id, message.usage) before summing:
|
|
290
|
+
# * For native Anthropic upstreams message.id is unique per turn, so
|
|
291
|
+
# this collapses the content-block fan-out cleanly.
|
|
292
|
+
# * For non-Anthropic upstreams that round-trip through the router's
|
|
293
|
+
# translator, message.id can be a constant placeholder
|
|
294
|
+
# ("msg_translated"); usage still differs per turn (input_tokens
|
|
295
|
+
# grows), so the composite key keeps turns distinct. Two turns with
|
|
296
|
+
# byte-identical id AND usage would still collapse, but that's a
|
|
297
|
+
# genuine retry/duplicate we want to drop.
|
|
275
298
|
read -r session_savings tot_in tot_out tot_cache_read tot_cache_write < <(
|
|
276
|
-
jq -
|
|
277
|
-
select(.type=="assistant") |
|
|
299
|
+
jq -rs --argjson p "$prices" --arg requested "$requested_norm" '
|
|
300
|
+
[.[] | select(.type=="assistant")] |
|
|
301
|
+
unique_by([.message.id, .message.usage]) |
|
|
302
|
+
.[] |
|
|
278
303
|
.message as $m |
|
|
279
304
|
($m.model // "" | sub("\\[[^]]*\\]$"; "") | sub("-[0-9]{8}$"; "")) as $rm |
|
|
280
305
|
{
|
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
|
|
@@ -47,11 +54,14 @@ non_interactive="false"
|
|
|
47
54
|
quiet="false"
|
|
48
55
|
router_key_header="X-Weave-Router-Key"
|
|
49
56
|
# Target tool whose config we patch. "claude" (default) writes Claude Code
|
|
50
|
-
# settings.json; "codex" writes ~/.codex/config.toml
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
57
|
+
# settings.json; "codex" writes ~/.codex/config.toml; "opencode" merges a
|
|
58
|
+
# provider block into opencode.json. Each target carries its own
|
|
59
|
+
# credential-passthrough story in the router: Claude Code's logged-in
|
|
60
|
+
# Anthropic key flows through unchanged, Codex's `OPENAI_API_KEY` flows
|
|
61
|
+
# through via the same header path, and opencode talks to the router via
|
|
62
|
+
# its anthropic-compatible API surface. target_explicit tracks whether
|
|
63
|
+
# --claude / --codex / --opencode was passed so an interactive run can
|
|
64
|
+
# prompt for the choice.
|
|
55
65
|
target="claude"
|
|
56
66
|
target_explicit="false"
|
|
57
67
|
|
|
@@ -97,8 +107,9 @@ print_banner() {
|
|
|
97
107
|
[ "$tty_out" = "true" ] || return 0
|
|
98
108
|
local target_label
|
|
99
109
|
case "$target" in
|
|
100
|
-
codex)
|
|
101
|
-
|
|
110
|
+
codex) target_label="Codex installer" ;;
|
|
111
|
+
opencode) target_label="opencode installer" ;;
|
|
112
|
+
*) target_label="Claude Code installer" ;;
|
|
102
113
|
esac
|
|
103
114
|
printf '\n'
|
|
104
115
|
printf '%s ╦ ╦╔═╗╔═╗╦ ╦╔═╗%s\n' "$C_BRAND" "$C_RESET"
|
|
@@ -395,6 +406,10 @@ write_codex_config() {
|
|
|
395
406
|
if [ -n "$block_name" ]; then
|
|
396
407
|
headers_parts="${headers_parts}, \"X-Weave-User-Name\" = \"${esc_name}\""
|
|
397
408
|
fi
|
|
409
|
+
# Tag the client so telemetry can attribute traffic to Codex vs other CLIs
|
|
410
|
+
# that share the same router key. The router otherwise has to guess from
|
|
411
|
+
# User-Agent.
|
|
412
|
+
headers_parts="${headers_parts}, \"X-App\" = \"codex\""
|
|
398
413
|
local headers_line="http_headers = { ${headers_parts} }"
|
|
399
414
|
|
|
400
415
|
local block
|
|
@@ -469,6 +484,92 @@ TOML
|
|
|
469
484
|
chmod 600 "$config_file"
|
|
470
485
|
}
|
|
471
486
|
|
|
487
|
+
# write_opencode_config merges a managed `provider.weave` entry into opencode's
|
|
488
|
+
# opencode.json (anthropic-compatible — the router speaks the Anthropic
|
|
489
|
+
# Messages API natively, so opencode's bundled @ai-sdk/anthropic provider
|
|
490
|
+
# works unmodified). Re-running rewrites the block in-place via jq; uninstall
|
|
491
|
+
# strips it the same way. We also set `model` at the top level so a fresh
|
|
492
|
+
# `opencode` invocation picks the router by default; if the user has set
|
|
493
|
+
# their own model already, we leave it alone.
|
|
494
|
+
#
|
|
495
|
+
# Usage: write_opencode_config <config_file_path> <base_url> <api_key> [user_email] [user_name]
|
|
496
|
+
write_opencode_config() {
|
|
497
|
+
local config_file="$1"
|
|
498
|
+
local block_url="$2"
|
|
499
|
+
local block_key="$3"
|
|
500
|
+
local block_email="${4:-}"
|
|
501
|
+
local block_name="${5:-}"
|
|
502
|
+
|
|
503
|
+
# Build the headers object piecewise so empty email/name vanish from the
|
|
504
|
+
# final JSON. opencode forwards the `headers` map verbatim to the upstream
|
|
505
|
+
# provider, so the router sees the same X-Weave-* triplet here that it
|
|
506
|
+
# would from Claude Code or Codex. The X-App tag lets router telemetry
|
|
507
|
+
# attribute traffic to opencode specifically.
|
|
508
|
+
local headers_json
|
|
509
|
+
headers_json="$(jq -n \
|
|
510
|
+
--arg key "$block_key" \
|
|
511
|
+
--arg email "$block_email" \
|
|
512
|
+
--arg name "$block_name" '
|
|
513
|
+
{"X-Weave-Router-Key": $key, "X-App": "opencode"}
|
|
514
|
+
| (if $email != "" then . + {"X-Weave-User-Email": $email} else . end)
|
|
515
|
+
| (if $name != "" then . + {"X-Weave-User-Name": $name } else . end)
|
|
516
|
+
')"
|
|
517
|
+
|
|
518
|
+
# Headline models we surface in opencode's picker. The router re-routes
|
|
519
|
+
# each request anyway, so this list is mostly UX — what shows up when the
|
|
520
|
+
# user runs /models inside opencode. Keep it short and Anthropic-shaped
|
|
521
|
+
# so the bundled @ai-sdk/anthropic provider can request them.
|
|
522
|
+
#
|
|
523
|
+
# apiKey is set to the router key as well as planted in headers. opencode's
|
|
524
|
+
# @ai-sdk/anthropic provider treats apiKey as required at config-parse time
|
|
525
|
+
# and otherwise falls back to ANTHROPIC_API_KEY from the environment;
|
|
526
|
+
# without this, a user who's never had an Anthropic key in their shell hits
|
|
527
|
+
# a startup error before the router ever sees a request. The router itself
|
|
528
|
+
# ignores the value (auth runs off X-Weave-Router-Key); apiKey here is just
|
|
529
|
+
# a placeholder that satisfies the SDK's "is auth configured" check.
|
|
530
|
+
local block
|
|
531
|
+
block="$(jq -n \
|
|
532
|
+
--arg url "$block_url/v1" \
|
|
533
|
+
--arg key "$block_key" \
|
|
534
|
+
--argjson headers "$headers_json" '
|
|
535
|
+
{
|
|
536
|
+
npm: "@ai-sdk/anthropic",
|
|
537
|
+
name: "Weave Router",
|
|
538
|
+
options: { apiKey: $key, baseURL: $url, headers: $headers },
|
|
539
|
+
models: {
|
|
540
|
+
"claude-opus-4-7": { name: "Claude Opus 4.7 (via Weave Router)" },
|
|
541
|
+
"claude-sonnet-4-6": { name: "Claude Sonnet 4.6 (via Weave Router)" },
|
|
542
|
+
"claude-haiku-4-5": { name: "Claude Haiku 4.5 (via Weave Router)" }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
')"
|
|
546
|
+
|
|
547
|
+
# Merge into any existing opencode.json. We always overwrite provider.weave
|
|
548
|
+
# so re-install reflects the latest key/identity, but we leave the rest of
|
|
549
|
+
# the file (other providers, mcp, agent settings) untouched. Top-level
|
|
550
|
+
# `model` is only set when the user hasn't already picked one.
|
|
551
|
+
local merged
|
|
552
|
+
if [ -f "$config_file" ]; then
|
|
553
|
+
merged="$(jq --argjson block "$block" '
|
|
554
|
+
.provider = ((.provider // {}) | .weave = $block)
|
|
555
|
+
| (if (.model // "") == "" then .model = "weave/claude-sonnet-4-6" else . end)
|
|
556
|
+
| (.["$schema"] //= "https://opencode.ai/config.json")
|
|
557
|
+
' "$config_file")"
|
|
558
|
+
else
|
|
559
|
+
merged="$(jq -n --argjson block "$block" '
|
|
560
|
+
{
|
|
561
|
+
"$schema": "https://opencode.ai/config.json",
|
|
562
|
+
model: "weave/claude-sonnet-4-6",
|
|
563
|
+
provider: { weave: $block }
|
|
564
|
+
}
|
|
565
|
+
')"
|
|
566
|
+
fi
|
|
567
|
+
printf '%s\n' "$merged" >"$config_file"
|
|
568
|
+
# 0600: the file holds a router key. Even at user scope, mode 644 would
|
|
569
|
+
# leak the key to any local user on a shared box.
|
|
570
|
+
chmod 600 "$config_file"
|
|
571
|
+
}
|
|
572
|
+
|
|
472
573
|
# resolve_user_name mirrors resolve_user_email but for display name. Priority:
|
|
473
574
|
# WEAVE_USER_NAME env override → git config user.name → empty. We don't
|
|
474
575
|
# prompt for name independently: if email prompting yielded nothing, name
|
|
@@ -571,9 +672,13 @@ while [ $# -gt 0 ]; do
|
|
|
571
672
|
--codex)
|
|
572
673
|
target="codex"; target_explicit="true"; shift
|
|
573
674
|
;;
|
|
675
|
+
--opencode)
|
|
676
|
+
target="opencode"; target_explicit="true"; shift
|
|
677
|
+
;;
|
|
574
678
|
--claude)
|
|
575
|
-
# No-op selector for symmetry with --codex. Useful in
|
|
576
|
-
# want to skip the interactive picker without depending
|
|
679
|
+
# No-op selector for symmetry with --codex / --opencode. Useful in
|
|
680
|
+
# pipelines that want to skip the interactive picker without depending
|
|
681
|
+
# on the default.
|
|
577
682
|
target="claude"; target_explicit="true"; shift
|
|
578
683
|
;;
|
|
579
684
|
-h|--help)
|
|
@@ -603,11 +708,13 @@ if [ "$target_explicit" = "false" ] && [ "$non_interactive" = "false" ] && [ -r
|
|
|
603
708
|
printf "%sInstall target:%s\n" "$C_BOLD" "$C_RESET"
|
|
604
709
|
printf " %s1)%s Claude Code %s— patches ~/.claude/settings.json (or <repo>/.claude/)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
605
710
|
printf " %s2)%s Codex %s— patches ~/.codex/config.toml (or <repo>/.codex/)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
606
|
-
printf "
|
|
711
|
+
printf " %s3)%s opencode %s— patches ~/.config/opencode/opencode.json (or <repo>/opencode.json)%s\n" "$C_BRAND" "$C_RESET" "$C_DIM" "$C_RESET"
|
|
712
|
+
printf "Choose %s[1/2/3]%s (default %s1%s): " "$C_BOLD" "$C_RESET" "$C_BOLD" "$C_RESET"
|
|
607
713
|
read -r target_choice </dev/tty || target_choice=""
|
|
608
714
|
case "${target_choice:-1}" in
|
|
609
715
|
1|""|claude|c|C) target="claude" ;;
|
|
610
716
|
2|codex|x|X) target="codex" ;;
|
|
717
|
+
3|opencode|o|O) target="opencode" ;;
|
|
611
718
|
*) err "invalid choice: $target_choice"; exit 2 ;;
|
|
612
719
|
esac
|
|
613
720
|
fi
|
|
@@ -631,6 +738,19 @@ if [ -z "$install_dir" ] && [ "$scope_explicit" = "false" ] && [ "$non_interacti
|
|
|
631
738
|
scope_project_path="<repo>/.codex/"
|
|
632
739
|
scope_cli_label="codex"
|
|
633
740
|
;;
|
|
741
|
+
opencode)
|
|
742
|
+
# Match the actual install path, which honors XDG_CONFIG_HOME. Showing a
|
|
743
|
+
# hardcoded "~/.config/opencode/" here lied to users with a custom
|
|
744
|
+
# $XDG_CONFIG_HOME — they'd see one path in the prompt and the installer
|
|
745
|
+
# would write to another.
|
|
746
|
+
if [ -n "${XDG_CONFIG_HOME:-}" ]; then
|
|
747
|
+
scope_user_path="$XDG_CONFIG_HOME/opencode/"
|
|
748
|
+
else
|
|
749
|
+
scope_user_path="~/.config/opencode/"
|
|
750
|
+
fi
|
|
751
|
+
scope_project_path="<repo>/opencode.json"
|
|
752
|
+
scope_cli_label="opencode"
|
|
753
|
+
;;
|
|
634
754
|
*)
|
|
635
755
|
scope_user_path="~/.claude/"
|
|
636
756
|
scope_project_path="<repo>/.claude/"
|
|
@@ -674,8 +794,9 @@ fi
|
|
|
674
794
|
[ "$quiet" = "true" ] || info "scope=${C_BOLD}${scope}${C_RESET} target=${C_BOLD}${target}${C_RESET} base_url=${C_BOLD}${base_url}${C_RESET}"
|
|
675
795
|
|
|
676
796
|
# Codex install only writes a TOML file (managed via awk) so jq isn't needed.
|
|
677
|
-
# Claude Code's settings.json
|
|
678
|
-
|
|
797
|
+
# Claude Code's settings.json and opencode's opencode.json patching both use
|
|
798
|
+
# jq to deep-merge / structurally rewrite JSON.
|
|
799
|
+
if [ "$target" = "claude" ] || [ "$target" = "opencode" ]; then
|
|
679
800
|
require_cmd jq "macOS: 'brew install jq' · Debian/Ubuntu: 'sudo apt install jq'"
|
|
680
801
|
fi
|
|
681
802
|
require_cmd curl "macOS/Linux: usually preinstalled — check your package manager"
|
|
@@ -693,6 +814,12 @@ case "$target" in
|
|
|
693
814
|
warn "Continuing — config.toml will be written and will take effect once Codex is installed."
|
|
694
815
|
fi
|
|
695
816
|
;;
|
|
817
|
+
opencode)
|
|
818
|
+
if ! command -v opencode >/dev/null 2>&1; then
|
|
819
|
+
warn "'opencode' not found on PATH. Install from https://opencode.ai (or 'npm install -g opencode-ai'), then re-run this script."
|
|
820
|
+
warn "Continuing — opencode.json will be written and will take effect once opencode is installed."
|
|
821
|
+
fi
|
|
822
|
+
;;
|
|
696
823
|
esac
|
|
697
824
|
|
|
698
825
|
script_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
|
@@ -763,7 +890,7 @@ if [ "$target" = "claude" ]; then
|
|
|
763
890
|
fi
|
|
764
891
|
|
|
765
892
|
mkdir -p "$settings_dir" "$statusline_dir"
|
|
766
|
-
|
|
893
|
+
elif [ "$target" = "codex" ]; then
|
|
767
894
|
# Codex CLI reads config from ~/.codex/config.toml by default. For project
|
|
768
895
|
# scope we write to <repo>/.codex/config.toml; the user invokes Codex with
|
|
769
896
|
# `CODEX_HOME=<repo>/.codex codex` (or runs from the repo if Codex auto-
|
|
@@ -778,6 +905,34 @@ else
|
|
|
778
905
|
fi
|
|
779
906
|
|
|
780
907
|
mkdir -p "$codex_dir"
|
|
908
|
+
else
|
|
909
|
+
# opencode discovers config in this order: $XDG_CONFIG_HOME/opencode/opencode.json
|
|
910
|
+
# (or ~/.config/opencode/opencode.json) for user scope, and opencode.json /
|
|
911
|
+
# opencode.jsonc walked up from CWD for project scope. We standardize on
|
|
912
|
+
# opencode.json at the repo root for project scope (the option teammates can
|
|
913
|
+
# commit) and the XDG path for user scope. The router key is embedded so
|
|
914
|
+
# opencode.json goes in .gitignore for project scope, same as Codex.
|
|
915
|
+
case "$scope" in
|
|
916
|
+
user)
|
|
917
|
+
opencode_dir="${XDG_CONFIG_HOME:-$settings_base/.config}/opencode"
|
|
918
|
+
;;
|
|
919
|
+
project)
|
|
920
|
+
opencode_dir="$settings_base"
|
|
921
|
+
;;
|
|
922
|
+
esac
|
|
923
|
+
# --dir overrides both scopes: drop opencode.json straight into <dir>/ so
|
|
924
|
+
# the sandbox is self-contained (mirrors how --dir behaves for Codex).
|
|
925
|
+
if [ -n "$install_dir" ]; then
|
|
926
|
+
opencode_dir="$install_dir"
|
|
927
|
+
fi
|
|
928
|
+
opencode_config_file="$opencode_dir/opencode.json"
|
|
929
|
+
|
|
930
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
931
|
+
refuse_if_symlink "$opencode_dir"
|
|
932
|
+
refuse_if_symlink "$opencode_config_file"
|
|
933
|
+
fi
|
|
934
|
+
|
|
935
|
+
mkdir -p "$opencode_dir"
|
|
781
936
|
fi
|
|
782
937
|
|
|
783
938
|
# ---------- token handling ----------
|
|
@@ -839,11 +994,57 @@ else
|
|
|
839
994
|
info "No identity set — router traffic will be attributed by account UUID only."
|
|
840
995
|
fi
|
|
841
996
|
|
|
997
|
+
# ---------- slash command wrappers (shared by both targets) ----------
|
|
998
|
+
#
|
|
999
|
+
# Claude Code intercepts any prompt starting with "/" as a local slash command,
|
|
1000
|
+
# so a typed /force-model would resolve to "Unknown command" and never reach
|
|
1001
|
+
# the router. Codex CLI does the same (its built-in / menu has its own set).
|
|
1002
|
+
# Drop wrapper markdown files into the per-target commands directory so the
|
|
1003
|
+
# slash invocation expands locally into a literal "/force-model …" prompt that
|
|
1004
|
+
# the router's first-line parser picks up.
|
|
1005
|
+
#
|
|
1006
|
+
# Layout:
|
|
1007
|
+
# Claude: <settings_dir>/commands/{force-model,unforce-model}.md → /force-model
|
|
1008
|
+
# Codex: <codex_dir>/prompts/{force-model,unforce-model}.md → /prompts:force-model
|
|
1009
|
+
#
|
|
1010
|
+
# Files come from install/commands/ in the repo (or the colocated commands/
|
|
1011
|
+
# directory the npm package ships alongside install.sh).
|
|
1012
|
+
install_slash_commands() {
|
|
1013
|
+
dst_dir="$1"
|
|
1014
|
+
commands_src_dir=""
|
|
1015
|
+
for candidate in \
|
|
1016
|
+
"$script_dir/commands" \
|
|
1017
|
+
"$script_dir/../commands"
|
|
1018
|
+
do
|
|
1019
|
+
if [ -d "$candidate" ]; then
|
|
1020
|
+
commands_src_dir="$candidate"
|
|
1021
|
+
break
|
|
1022
|
+
fi
|
|
1023
|
+
done
|
|
1024
|
+
[ -n "$commands_src_dir" ] || return 0
|
|
1025
|
+
|
|
1026
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1027
|
+
refuse_if_symlink "$dst_dir"
|
|
1028
|
+
fi
|
|
1029
|
+
mkdir -p "$dst_dir"
|
|
1030
|
+
for cmd in force-model unforce-model; do
|
|
1031
|
+
src="$commands_src_dir/$cmd.md"
|
|
1032
|
+
dst="$dst_dir/$cmd.md"
|
|
1033
|
+
[ -f "$src" ] || continue
|
|
1034
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1035
|
+
refuse_if_symlink "$dst"
|
|
1036
|
+
fi
|
|
1037
|
+
cp "$src" "$dst"
|
|
1038
|
+
done
|
|
1039
|
+
ok "Slash commands written to $dst_dir (force-model, unforce-model)"
|
|
1040
|
+
}
|
|
1041
|
+
|
|
842
1042
|
# ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
|
|
843
1043
|
|
|
844
1044
|
if [ "$target" = "codex" ]; then
|
|
845
1045
|
write_codex_config "$codex_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
846
1046
|
ok "Codex config written to $codex_config_file"
|
|
1047
|
+
install_slash_commands "$codex_dir/prompts"
|
|
847
1048
|
|
|
848
1049
|
# Project scope: ensure the per-teammate config (which holds the router key)
|
|
849
1050
|
# is gitignored. The base URL is the same for every teammate, so a
|
|
@@ -893,6 +1094,56 @@ if [ "$target" = "codex" ]; then
|
|
|
893
1094
|
exit 0
|
|
894
1095
|
fi
|
|
895
1096
|
|
|
1097
|
+
# ---------- opencode install path (dispatch + exit before the Claude-only writes) ----------
|
|
1098
|
+
|
|
1099
|
+
if [ "$target" = "opencode" ]; then
|
|
1100
|
+
write_opencode_config "$opencode_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
1101
|
+
ok "opencode config written to $opencode_config_file"
|
|
1102
|
+
|
|
1103
|
+
# Project scope: the per-teammate config carries the router key, so it
|
|
1104
|
+
# stays out of git. Same reasoning as the Codex path — base URL is shared,
|
|
1105
|
+
# but the key is per-person.
|
|
1106
|
+
if [ "$scope" = "project" ] && [ -z "$install_dir" ] && [ -n "${git_root:-}" ]; then
|
|
1107
|
+
gitignore="$git_root/.gitignore"
|
|
1108
|
+
refuse_if_symlink "$gitignore"
|
|
1109
|
+
for entry in \
|
|
1110
|
+
"opencode.json"
|
|
1111
|
+
do
|
|
1112
|
+
if [ ! -f "$gitignore" ] || ! grep -qxF "$entry" "$gitignore"; then
|
|
1113
|
+
printf '%s\n' "$entry" >>"$gitignore"
|
|
1114
|
+
fi
|
|
1115
|
+
done
|
|
1116
|
+
ok "Updated $gitignore (ignored opencode.json)"
|
|
1117
|
+
fi
|
|
1118
|
+
|
|
1119
|
+
# Post-install verification: same probes the Claude/Codex paths run.
|
|
1120
|
+
if [ "$quiet" != "true" ]; then
|
|
1121
|
+
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
1122
|
+
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
1123
|
+
fi
|
|
1124
|
+
fi
|
|
1125
|
+
|
|
1126
|
+
if [ -n "$api_key" ]; then
|
|
1127
|
+
validate_opencode_key() {
|
|
1128
|
+
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
1129
|
+
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
1130
|
+
}
|
|
1131
|
+
if ! spin "Validating API key" validate_opencode_key; then
|
|
1132
|
+
warn "Router rejected the API key (check it matches the dashboard at $base_url/ui/)."
|
|
1133
|
+
fi
|
|
1134
|
+
fi
|
|
1135
|
+
|
|
1136
|
+
printf "\n"
|
|
1137
|
+
printf "%s✓%s %s%sWeave Router installed for opencode.%s\n" \
|
|
1138
|
+
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
1139
|
+
if [ -n "$install_dir" ]; then
|
|
1140
|
+
# --dir installs land outside opencode's discovery roots, so the caller
|
|
1141
|
+
# has to point opencode at the file explicitly.
|
|
1142
|
+
info "Run opencode with OPENCODE_CONFIG=$opencode_config_file opencode."
|
|
1143
|
+
fi
|
|
1144
|
+
exit 0
|
|
1145
|
+
fi
|
|
1146
|
+
|
|
896
1147
|
# ---------- write the statusline script ----------
|
|
897
1148
|
|
|
898
1149
|
cat > "$statusline_file" << 'STATUSLINE_EOF'
|
|
@@ -1043,6 +1294,7 @@ prices='{
|
|
|
1043
1294
|
"gemini-3-pro-preview": 0.002,
|
|
1044
1295
|
"gemini-3.1-flash-lite-preview": 0.0001,
|
|
1045
1296
|
"gemini-3.1-pro-preview": 0.002,
|
|
1297
|
+
"gemini-3.5-flash": 0.0015,
|
|
1046
1298
|
"gpt-4.1": 0.002,
|
|
1047
1299
|
"gpt-4.1-mini": 0.0004,
|
|
1048
1300
|
"gpt-4.1-nano": 0.0001,
|
|
@@ -1061,11 +1313,15 @@ prices='{
|
|
|
1061
1313
|
"gpt-5.5-nano": 0.00015,
|
|
1062
1314
|
"gpt-5.5-pro": 0.03,
|
|
1063
1315
|
"minimax/minimax-m2.7": 0.0003,
|
|
1316
|
+
"mistralai/mistral-small-2603": 0.0002,
|
|
1064
1317
|
"moonshotai/kimi-k2.5": 0.0006,
|
|
1065
1318
|
"moonshotai/kimi-k2.6": 0.00095,
|
|
1066
1319
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
1320
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.00015,
|
|
1321
|
+
"qwen/qwen3-coder": 0.0009,
|
|
1067
1322
|
"qwen/qwen3-coder-next": 0.0005,
|
|
1068
1323
|
"qwen/qwen3-next-80b-a3b-instruct": 0.00015,
|
|
1324
|
+
"qwen/qwen3.5-flash-02-23": 0.00005,
|
|
1069
1325
|
"qwen/qwen3.6-35b-a3b": 0.00015,
|
|
1070
1326
|
"xiaomi/mimo-v2.5": 0.0004,
|
|
1071
1327
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
@@ -1088,6 +1344,7 @@ prices='{
|
|
|
1088
1344
|
"gemini-3-pro-preview": 0.008,
|
|
1089
1345
|
"gemini-3.1-flash-lite-preview": 0.0004,
|
|
1090
1346
|
"gemini-3.1-pro-preview": 0.008,
|
|
1347
|
+
"gemini-3.5-flash": 0.009,
|
|
1091
1348
|
"gpt-4.1": 0.008,
|
|
1092
1349
|
"gpt-4.1-mini": 0.0016,
|
|
1093
1350
|
"gpt-4.1-nano": 0.0004,
|
|
@@ -1106,11 +1363,15 @@ prices='{
|
|
|
1106
1363
|
"gpt-5.5-nano": 0.0006,
|
|
1107
1364
|
"gpt-5.5-pro": 0.12,
|
|
1108
1365
|
"minimax/minimax-m2.7": 0.0012,
|
|
1366
|
+
"mistralai/mistral-small-2603": 0.0006,
|
|
1109
1367
|
"moonshotai/kimi-k2.5": 0.003,
|
|
1110
1368
|
"moonshotai/kimi-k2.6": 0.004,
|
|
1111
1369
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
1370
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.0006,
|
|
1371
|
+
"qwen/qwen3-coder": 0.0027,
|
|
1112
1372
|
"qwen/qwen3-coder-next": 0.0012,
|
|
1113
1373
|
"qwen/qwen3-next-80b-a3b-instruct": 0.0012,
|
|
1374
|
+
"qwen/qwen3.5-flash-02-23": 0.00015,
|
|
1114
1375
|
"qwen/qwen3.6-35b-a3b": 0.00095,
|
|
1115
1376
|
"xiaomi/mimo-v2.5": 0.002,
|
|
1116
1377
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
@@ -1170,9 +1431,24 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
1170
1431
|
# The marker regex tolerates the optional "(<provider>)" segment and a
|
|
1171
1432
|
# `[1m]` / `-YYYYMMDD` suffix on either model name so transcripts written
|
|
1172
1433
|
# against context-tiered or dated model ids still parse cleanly.
|
|
1434
|
+
#
|
|
1435
|
+
# Dedup note: CC writes one JSONL entry per *content block* in an
|
|
1436
|
+
# assistant turn (text, text, tool_use → 3 entries), and every entry
|
|
1437
|
+
# carries the same `message.usage`. Summing per-entry triple-counts the
|
|
1438
|
+
# turn. We dedupe on (message.id, message.usage) before summing:
|
|
1439
|
+
# * For native Anthropic upstreams message.id is unique per turn, so
|
|
1440
|
+
# this collapses the content-block fan-out cleanly.
|
|
1441
|
+
# * For non-Anthropic upstreams that round-trip through the router's
|
|
1442
|
+
# translator, message.id can be a constant placeholder
|
|
1443
|
+
# ("msg_translated"); usage still differs per turn (input_tokens
|
|
1444
|
+
# grows), so the composite key keeps turns distinct. Two turns with
|
|
1445
|
+
# byte-identical id AND usage would still collapse, but that's a
|
|
1446
|
+
# genuine retry/duplicate we want to drop.
|
|
1173
1447
|
read -r session_savings tot_in tot_out tot_cache_read tot_cache_write < <(
|
|
1174
|
-
jq -
|
|
1175
|
-
select(.type=="assistant") |
|
|
1448
|
+
jq -rs --argjson p "$prices" --arg requested "$requested_norm" '
|
|
1449
|
+
[.[] | select(.type=="assistant")] |
|
|
1450
|
+
unique_by([.message.id, .message.usage]) |
|
|
1451
|
+
.[] |
|
|
1176
1452
|
.message as $m |
|
|
1177
1453
|
($m.model // "" | sub("\\[[^]]*\\]$"; "") | sub("-[0-9]{8}$"; "")) as $rm |
|
|
1178
1454
|
{
|
|
@@ -1297,6 +1573,7 @@ fi
|
|
|
1297
1573
|
if [ -n "$user_name" ]; then
|
|
1298
1574
|
custom_headers="$custom_headers"$'\n'"X-Weave-User-Name: $user_name"
|
|
1299
1575
|
fi
|
|
1576
|
+
custom_headers="$custom_headers"$'\n'"X-App: claude-code"
|
|
1300
1577
|
|
|
1301
1578
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
1302
1579
|
jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
|
|
@@ -1329,6 +1606,9 @@ else
|
|
|
1329
1606
|
fi
|
|
1330
1607
|
ok "Settings written to $settings_file"
|
|
1331
1608
|
|
|
1609
|
+
# Slash command wrappers — see install_slash_commands() below for the why.
|
|
1610
|
+
install_slash_commands "$settings_dir/commands"
|
|
1611
|
+
|
|
1332
1612
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
1333
1613
|
jq -n --arg header "$custom_headers" '{
|
|
1334
1614
|
env: { ANTHROPIC_CUSTOM_HEADERS: $header }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workweave/router",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "One-command installer that points Claude Code at the Weave Router.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"weave-router": "bin.js"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"install.sh",
|
|
11
11
|
"uninstall.sh",
|
|
12
12
|
"cc-statusline.sh",
|
|
13
|
+
"commands/",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE"
|
|
15
16
|
],
|
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,82 @@ 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
|
+
if [ -n "$install_dir" ]; then
|
|
177
|
+
ok "Weave Router uninstalled from $install_dir (opencode)."
|
|
178
|
+
else
|
|
179
|
+
ok "Weave Router uninstalled (opencode, scope=$scope)."
|
|
180
|
+
fi
|
|
181
|
+
exit 0
|
|
182
|
+
fi
|
|
183
|
+
|
|
100
184
|
# ---------- codex uninstall path ----------
|
|
101
185
|
|
|
102
186
|
if [ "$target" = "codex" ]; then
|
|
@@ -156,6 +240,22 @@ if [ "$target" = "codex" ]; then
|
|
|
156
240
|
info "No Codex config at $codex_config_file (already uninstalled?)"
|
|
157
241
|
fi
|
|
158
242
|
|
|
243
|
+
# Remove only the prompt files this installer owns; leave any user-authored
|
|
244
|
+
# entries in prompts/ alone. The dir itself is dropped only if empty after.
|
|
245
|
+
codex_prompts_dir="$codex_dir/prompts"
|
|
246
|
+
if [ -d "$codex_prompts_dir" ]; then
|
|
247
|
+
refuse_if_symlink "$codex_prompts_dir"
|
|
248
|
+
for cmd in force-model unforce-model; do
|
|
249
|
+
cmd_file="$codex_prompts_dir/$cmd.md"
|
|
250
|
+
if [ -f "$cmd_file" ]; then
|
|
251
|
+
refuse_if_symlink "$cmd_file"
|
|
252
|
+
rm -f "$cmd_file"
|
|
253
|
+
ok "Removed $cmd_file"
|
|
254
|
+
fi
|
|
255
|
+
done
|
|
256
|
+
rmdir "$codex_prompts_dir" 2>/dev/null || true
|
|
257
|
+
fi
|
|
258
|
+
|
|
159
259
|
if [ -n "$install_dir" ]; then
|
|
160
260
|
ok "Weave Router uninstalled from $install_dir (Codex)."
|
|
161
261
|
else
|
|
@@ -269,6 +369,24 @@ for f in "$statusline_file"; do
|
|
|
269
369
|
fi
|
|
270
370
|
done
|
|
271
371
|
|
|
372
|
+
# Remove only the slash command files this installer owns; leave any other
|
|
373
|
+
# files in commands/ alone. The directory itself stays if it still contains
|
|
374
|
+
# unrelated user commands.
|
|
375
|
+
commands_dir="$(dirname "$settings_file")/commands"
|
|
376
|
+
if [ -d "$commands_dir" ]; then
|
|
377
|
+
refuse_if_symlink "$commands_dir"
|
|
378
|
+
for cmd in force-model unforce-model; do
|
|
379
|
+
cmd_file="$commands_dir/$cmd.md"
|
|
380
|
+
if [ -f "$cmd_file" ]; then
|
|
381
|
+
refuse_if_symlink "$cmd_file"
|
|
382
|
+
rm -f "$cmd_file"
|
|
383
|
+
ok "Removed $cmd_file"
|
|
384
|
+
fi
|
|
385
|
+
done
|
|
386
|
+
# Clean up the dir only if we left nothing behind.
|
|
387
|
+
rmdir "$commands_dir" 2>/dev/null || true
|
|
388
|
+
fi
|
|
389
|
+
|
|
272
390
|
if [ -n "$install_dir" ]; then
|
|
273
391
|
ok "Weave Router uninstalled from $install_dir."
|
|
274
392
|
else
|