@workweave/router 0.2.11 → 0.2.12
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 +25 -11
- package/cc-statusline.sh +95 -16
- package/codex-skills/fm/SKILL.md +15 -0
- package/codex-skills/fm/scripts/emit.sh +8 -0
- package/codex-skills/force-model/SKILL.md +8 -7
- package/codex-skills/force-model/scripts/emit.sh +9 -0
- package/codex-skills/rf/SKILL.md +15 -0
- package/codex-skills/rf/scripts/emit.sh +7 -0
- package/codex-skills/router-feedback/SKILL.md +8 -7
- package/codex-skills/router-feedback/scripts/emit.sh +9 -0
- package/codex-skills/router-models/SKILL.md +51 -0
- package/codex-skills/router-off/SKILL.md +22 -0
- package/codex-skills/router-on/SKILL.md +22 -0
- package/codex-skills/router-status/SKILL.md +19 -0
- package/codex-skills/ufm/SKILL.md +14 -0
- package/codex-skills/ufm/scripts/emit.sh +3 -0
- package/codex-skills/unforce-model/SKILL.md +6 -6
- package/codex-skills/unforce-model/scripts/emit.sh +4 -0
- package/codex-status.sh +312 -0
- package/commands/beta.md +5 -0
- package/directives.tsv +5 -4
- package/install.sh +882 -254
- package/package.json +2 -1
- package/pi-router/README.md +3 -0
- package/pi-router/src/beta.ts +21 -0
- package/pi-router/src/index.ts +2 -0
- package/pi-router/src/pricing.generated.ts +77 -75
- package/pi-router/src/savings.ts +1 -1
- package/registry.sh +2 -0
- package/uninstall.sh +41 -5
package/install.sh
CHANGED
|
@@ -49,10 +49,11 @@
|
|
|
49
49
|
# npx @workweave/router --uninstall # remove a previous install (delegates to uninstall.sh)
|
|
50
50
|
#
|
|
51
51
|
# Re-running the installer reuses the key already on disk, so you only paste it
|
|
52
|
-
# once. `update` is the scriptable form
|
|
53
|
-
# managed config + assets in place, and
|
|
54
|
-
# can be found:
|
|
52
|
+
# once — for every client, not just Claude Code. `update` is the scriptable form
|
|
53
|
+
# of that: it never prompts, refreshes the managed config + assets in place, and
|
|
54
|
+
# errors (rather than asking) when no key can be found:
|
|
55
55
|
# npx @workweave/router update --claude # refresh the Claude Code install in place
|
|
56
|
+
# npx @workweave/router update --codex # same for Codex / opencode / pi
|
|
56
57
|
#
|
|
57
58
|
# Toggle an existing install on/off without losing the router config (so
|
|
58
59
|
# switching back is instant). These never prompt for a key and require an
|
|
@@ -353,7 +354,6 @@ refuse_if_symlink() {
|
|
|
353
354
|
# --codex) can find and replace the block instead of duplicating it.
|
|
354
355
|
WEAVE_CODEX_BEGIN_MARKER="# >>> weave-router managed (do not edit between markers) >>>"
|
|
355
356
|
WEAVE_CODEX_END_MARKER="# <<< weave-router managed <<<"
|
|
356
|
-
WEAVE_CODEX_SKILL_MARKER="<!-- weave-router managed disable-routing skill -->"
|
|
357
357
|
|
|
358
358
|
# ---------- identity helpers ----------
|
|
359
359
|
#
|
|
@@ -502,11 +502,12 @@ write_codex_config() {
|
|
|
502
502
|
printf '%s' "${s//\"/\\\"}"
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
-
local esc_key esc_email esc_name esc_url
|
|
505
|
+
local esc_key esc_email esc_name esc_url esc_status
|
|
506
506
|
esc_key="$(toml_escape "$block_key")"
|
|
507
507
|
esc_email="$(toml_escape "$block_email")"
|
|
508
508
|
esc_name="$(toml_escape "$block_name")"
|
|
509
509
|
esc_url="$(toml_escape "$block_url")"
|
|
510
|
+
esc_status="$(toml_escape "$codex_status_file")"
|
|
510
511
|
|
|
511
512
|
# Plant whichever identity values we have alongside the router key so the
|
|
512
513
|
# router can attribute Codex traffic to a person on shared keys. Build the
|
|
@@ -528,12 +529,45 @@ write_codex_config() {
|
|
|
528
529
|
# reaches them. Every endpoint, hosted or self-hosted, uses its own default.
|
|
529
530
|
local headers_line="http_headers = { ${headers_parts} }"
|
|
530
531
|
|
|
532
|
+
local hook_feature_line="features.hooks = true"
|
|
533
|
+
local hook_block=""
|
|
534
|
+
local codex_hooks_enabled="true"
|
|
535
|
+
if [ -f "$config_file" ]; then
|
|
536
|
+
if awk '
|
|
537
|
+
!in_section && /^[[:space:]]*hooks[[:space:]]*=/ { conflict=1 }
|
|
538
|
+
$0 == "[hooks]" { conflict=1 }
|
|
539
|
+
/^[[:space:]]*\[/ { in_section=1 }
|
|
540
|
+
END { exit(conflict ? 0 : 1) }
|
|
541
|
+
' "$config_file"; then
|
|
542
|
+
codex_hooks_enabled="false"
|
|
543
|
+
hook_feature_line=""
|
|
544
|
+
fi
|
|
545
|
+
fi
|
|
546
|
+
if [ -f "$config_file" ] && grep -q '^\[features\]$' "$config_file"; then
|
|
547
|
+
hook_feature_line=""
|
|
548
|
+
fi
|
|
549
|
+
if [ "$codex_hooks_enabled" = "true" ]; then
|
|
550
|
+
hook_block="$(cat <<TOML
|
|
551
|
+
|
|
552
|
+
[[hooks.SessionStart]]
|
|
553
|
+
[[hooks.SessionStart.hooks]]
|
|
554
|
+
type = "command"
|
|
555
|
+
command = "${esc_status}"
|
|
556
|
+
|
|
557
|
+
[[hooks.Stop]]
|
|
558
|
+
[[hooks.Stop.hooks]]
|
|
559
|
+
type = "command"
|
|
560
|
+
command = "${esc_status}"
|
|
561
|
+
TOML
|
|
562
|
+
)"
|
|
563
|
+
fi
|
|
531
564
|
local block
|
|
532
565
|
block="$(cat <<TOML
|
|
533
566
|
${WEAVE_CODEX_BEGIN_MARKER}
|
|
534
567
|
# Managed by the Weave Router installer. Re-running the installer rewrites
|
|
535
568
|
# this block; \`./uninstall.sh --codex\` removes it. To opt out without
|
|
536
569
|
# uninstalling, change the model_provider value below.
|
|
570
|
+
${hook_feature_line}
|
|
537
571
|
model_provider = "weave"
|
|
538
572
|
|
|
539
573
|
[model_providers.weave]
|
|
@@ -542,6 +576,7 @@ base_url = "${esc_url}/v1"
|
|
|
542
576
|
wire_api = "responses"
|
|
543
577
|
requires_openai_auth = true
|
|
544
578
|
${headers_line}
|
|
579
|
+
${hook_block}
|
|
545
580
|
${WEAVE_CODEX_END_MARKER}
|
|
546
581
|
TOML
|
|
547
582
|
)"
|
|
@@ -596,6 +631,25 @@ TOML
|
|
|
596
631
|
else
|
|
597
632
|
printf "%s\n" "$block" >"$config_file"
|
|
598
633
|
fi
|
|
634
|
+
|
|
635
|
+
# If the user already has a [features] table, place our managed hook
|
|
636
|
+
# setting in that table instead of using a duplicate dotted key. Preserve an
|
|
637
|
+
# explicit user setting and mark only the line this installer owns.
|
|
638
|
+
if grep -q '^\[features\]$' "$config_file"; then
|
|
639
|
+
if ! awk '
|
|
640
|
+
/^\[features\]$/ { in_features=1; next }
|
|
641
|
+
/^\[/ { in_features=0 }
|
|
642
|
+
in_features && /^[[:space:]]*hooks[[:space:]]*=/ { found=1 }
|
|
643
|
+
END { exit(found ? 0 : 1) }
|
|
644
|
+
' "$config_file"; then
|
|
645
|
+
tmp="$(mktemp -t weave-codex-features.XXXXXX)"
|
|
646
|
+
awk '/^\[features\]$/ && !inserted { print; print "hooks = true # weave-router managed codex hooks"; inserted=1; next } { print }' "$config_file" >"$tmp"
|
|
647
|
+
mv "$tmp" "$config_file"
|
|
648
|
+
fi
|
|
649
|
+
fi
|
|
650
|
+
if [ "$codex_hooks_enabled" != "true" ]; then
|
|
651
|
+
warn "Existing Codex hooks configuration is not an inline array; preserving it and skipping the Weave status hooks. Routing is still enabled."
|
|
652
|
+
fi
|
|
599
653
|
# 0600: the file holds a router key. Even at user scope, mode 644 would
|
|
600
654
|
# leak the key to any local user on a shared box.
|
|
601
655
|
chmod 600 "$config_file"
|
|
@@ -1149,7 +1203,7 @@ if [ "$mode" != "install" ] && [ "$mode" != "update" ]; then
|
|
|
1149
1203
|
non_interactive="true"
|
|
1150
1204
|
if [ "$target_explicit" != "true" ]; then
|
|
1151
1205
|
if [ "$mode" = "models" ]; then
|
|
1152
|
-
err "'models' requires an explicit client: --claude."
|
|
1206
|
+
err "'models' requires an explicit client: --claude or --codex."
|
|
1153
1207
|
else
|
|
1154
1208
|
err "'$mode' requires an explicit client: --claude, --codex, or --opencode."
|
|
1155
1209
|
fi
|
|
@@ -1157,12 +1211,13 @@ if [ "$mode" != "install" ] && [ "$mode" != "update" ]; then
|
|
|
1157
1211
|
fi
|
|
1158
1212
|
fi
|
|
1159
1213
|
|
|
1160
|
-
#
|
|
1161
|
-
#
|
|
1162
|
-
#
|
|
1163
|
-
# to
|
|
1164
|
-
|
|
1165
|
-
|
|
1214
|
+
# `models` needs to resolve one install's endpoint + key. Claude Code and Codex
|
|
1215
|
+
# both expose readers for that (resolve_installed_endpoint / read_installed_key);
|
|
1216
|
+
# opencode and pi do not have the endpoint-trust story worked out yet, so they
|
|
1217
|
+
# still fail fast here rather than falling through to the toggle dispatch, which
|
|
1218
|
+
# would flip config nobody asked to change.
|
|
1219
|
+
if [ "$mode" = "models" ] && [ "$target" != "claude" ] && [ "$target" != "codex" ]; then
|
|
1220
|
+
err "'models' supports --claude and --codex only (it resolves the router endpoint from that client's config)."
|
|
1166
1221
|
exit 2
|
|
1167
1222
|
fi
|
|
1168
1223
|
|
|
@@ -1185,16 +1240,14 @@ if [ "$mode" = "update" ]; then
|
|
|
1185
1240
|
err "--rotate-key needs a prompt, which 'update' never issues. Re-run the installer without 'update'."
|
|
1186
1241
|
exit 2
|
|
1187
1242
|
fi
|
|
1188
|
-
if [ "$target" != "claude" ]; then
|
|
1189
|
-
err "'update' currently supports --claude only. Re-run the installer for $target (it reuses your installed key)."
|
|
1190
|
-
exit 2
|
|
1191
|
-
fi
|
|
1192
1243
|
fi
|
|
1193
1244
|
|
|
1194
1245
|
# Toggle verbs (off/on/status) aren't implemented for pi — its config is a
|
|
1195
1246
|
# structural models.json/settings.json merge, reversed by the uninstaller
|
|
1196
|
-
# rather than a single env/key line we can park and restore.
|
|
1197
|
-
|
|
1247
|
+
# rather than a single env/key line we can park and restore. `update` is not a
|
|
1248
|
+
# toggle: it rewrites that same structural config in place, exactly as install
|
|
1249
|
+
# does, so it belongs with install here.
|
|
1250
|
+
if [ "$mode" != "install" ] && [ "$mode" != "update" ] && [ "$target" = "pi" ]; then
|
|
1198
1251
|
err "Toggle verbs (off/on/status) aren't supported for --pi. Use 'npx @workweave/router --uninstall --pi' to remove, or re-run the installer to refresh."
|
|
1199
1252
|
exit 2
|
|
1200
1253
|
fi
|
|
@@ -1318,7 +1371,13 @@ fi
|
|
|
1318
1371
|
# Claude Code's settings.json and opencode's opencode.json patching both use
|
|
1319
1372
|
# jq to deep-merge / structurally rewrite JSON. Toggling those clients reads
|
|
1320
1373
|
# and rewrites the same JSON, so jq is required there too.
|
|
1321
|
-
|
|
1374
|
+
#
|
|
1375
|
+
# `models` is the exception for Codex: it renders and edits the router's JSON
|
|
1376
|
+
# payloads with jq regardless of which client's config supplied the endpoint,
|
|
1377
|
+
# so require it there too rather than dying mid-render on `jq: command not
|
|
1378
|
+
# found`. Installing Codex itself still needs no jq.
|
|
1379
|
+
if [ "$target" = "claude" ] || [ "$target" = "opencode" ] || [ "$target" = "pi" ] \
|
|
1380
|
+
|| { [ "$mode" = "models" ] && [ "$target" = "codex" ]; }; then
|
|
1322
1381
|
require_cmd jq "macOS: 'brew install jq' · Debian/Ubuntu: 'sudo apt install jq'"
|
|
1323
1382
|
fi
|
|
1324
1383
|
# curl is used by the install/update paths' health/validate probes and by every
|
|
@@ -1375,12 +1434,13 @@ WEAVE_REGISTRY_DATA=$(cat <<'WEAVE_REGISTRY_EOF'
|
|
|
1375
1434
|
force-model|fm|prompt|yes|yes|yes|yes|manual|command,skill
|
|
1376
1435
|
unforce-model|ufm|prompt|yes|yes|yes|yes|manual|command,skill
|
|
1377
1436
|
router-feedback|rf|prompt|yes|yes|yes|no|manual|command,skill
|
|
1378
|
-
router-off||local-toggle|yes|
|
|
1379
|
-
router-on||local-toggle|yes|
|
|
1380
|
-
router-status||local-toggle|yes|
|
|
1437
|
+
router-off||local-toggle|yes|yes|no|no|manual|command,skill
|
|
1438
|
+
router-on||local-toggle|yes|yes|no|no|manual|command,skill
|
|
1439
|
+
router-status||local-toggle|yes|yes|no|no|manual|command,skill
|
|
1381
1440
|
router-session||prompt|yes|no|no|no|manual|command
|
|
1382
|
-
router-models|models|local-toggle|yes|
|
|
1441
|
+
router-models|models|local-toggle|yes|yes|no|no|manual|command,skill
|
|
1383
1442
|
disable-routing||local-toggle|no|yes|no|no|manual|skill
|
|
1443
|
+
beta||prompt|yes|no|no|yes|manual|command
|
|
1384
1444
|
WEAVE_REGISTRY_EOF
|
|
1385
1445
|
)
|
|
1386
1446
|
|
|
@@ -1429,6 +1489,7 @@ weave_registry_skill_names() {
|
|
|
1429
1489
|
cursor) [ "$cursor" = yes ] || continue ;;
|
|
1430
1490
|
esac
|
|
1431
1491
|
printf '%s\n' "$canonical"
|
|
1492
|
+
[ -n "$aliases" ] && printf '%s\n' "$aliases" | tr ',' '\n'
|
|
1432
1493
|
done <<EOF
|
|
1433
1494
|
$(weave_registry_rows)
|
|
1434
1495
|
EOF
|
|
@@ -1448,6 +1509,7 @@ weave_registry_skill_assets() {
|
|
|
1448
1509
|
cursor) [ "$cursor" = yes ] || continue ;;
|
|
1449
1510
|
esac
|
|
1450
1511
|
printf '%s\n' "$canonical"
|
|
1512
|
+
[ -n "$aliases" ] && printf '%s\n' "$aliases" | tr ',' '\n'
|
|
1451
1513
|
done <<EOF
|
|
1452
1514
|
$(weave_registry_rows)
|
|
1453
1515
|
EOF
|
|
@@ -1544,10 +1606,18 @@ elif [ "$target" = "codex" ]; then
|
|
|
1544
1606
|
# teammate — .codex/config.toml goes in .gitignore in project scope.
|
|
1545
1607
|
codex_dir="$settings_base/.codex"
|
|
1546
1608
|
codex_config_file="$codex_dir/config.toml"
|
|
1609
|
+
if [ "$scope" = "user" ] && [ -z "$install_dir" ]; then
|
|
1610
|
+
codex_status_file="$settings_base/.weave/codex-status.sh"
|
|
1611
|
+
else
|
|
1612
|
+
codex_status_file="$codex_dir/weave-status.sh"
|
|
1613
|
+
fi
|
|
1614
|
+
codex_status_disabled_marker="$(dirname "$codex_status_file")/.weave-router-disabled"
|
|
1547
1615
|
|
|
1548
1616
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
1549
1617
|
refuse_if_symlink "$codex_dir"
|
|
1550
1618
|
refuse_if_symlink "$codex_config_file"
|
|
1619
|
+
refuse_if_symlink "$codex_status_file"
|
|
1620
|
+
refuse_if_symlink "$codex_status_disabled_marker"
|
|
1551
1621
|
fi
|
|
1552
1622
|
|
|
1553
1623
|
mkdir -p "$codex_dir"
|
|
@@ -1664,14 +1734,93 @@ claude_key_present() {
|
|
|
1664
1734
|
[ -n "$(read_claude_key "$1")" ]
|
|
1665
1735
|
}
|
|
1666
1736
|
|
|
1737
|
+
# key_source_is_own returns 0 when a config file is safe to read a router key
|
|
1738
|
+
# back out of. Codex, opencode, and pi all embed the key in a file that lives
|
|
1739
|
+
# INSIDE the project in project scope, and the installer gitignores each one —
|
|
1740
|
+
# so a file git tracks is by definition not this user's own install. A hostile
|
|
1741
|
+
# checkout could commit one carrying an attacker's rk_ key, and adopting it
|
|
1742
|
+
# would bill the developer's traffic to whoever wrote the repo. Same tracked-file
|
|
1743
|
+
# test models_endpoint_is_trusted applies to a planted endpoint.
|
|
1744
|
+
#
|
|
1745
|
+
# Claude Code deliberately does NOT go through this: read_installed_key is
|
|
1746
|
+
# documented to pick up a committed header from an older install, which is a
|
|
1747
|
+
# supported case there.
|
|
1748
|
+
key_source_is_own() {
|
|
1749
|
+
local f="$1"
|
|
1750
|
+
[ -f "$f" ] || return 1
|
|
1751
|
+
[ -L "$f" ] && return 1
|
|
1752
|
+
# Only project-scoped installs put the file somewhere a repo can reach.
|
|
1753
|
+
[ "$scope" = "project" ] && [ -z "$install_dir" ] || return 0
|
|
1754
|
+
command -v git >/dev/null 2>&1 || return 1
|
|
1755
|
+
git -C "$(dirname "$f")" ls-files --error-unmatch -- "$f" >/dev/null 2>&1 && return 1
|
|
1756
|
+
return 0
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
# read_codex_key prints the router key out of Codex's config.toml, or nothing.
|
|
1760
|
+
# Scoped to the managed block so a key-shaped string the user wrote elsewhere in
|
|
1761
|
+
# the file is never adopted. awk rather than jq on purpose: the Codex path is the
|
|
1762
|
+
# one target that doesn't require jq (see the require_cmd block above).
|
|
1763
|
+
read_codex_key() {
|
|
1764
|
+
local f="$1"
|
|
1765
|
+
key_source_is_own "$f" || return 0
|
|
1766
|
+
awk -v begin="$WEAVE_CODEX_BEGIN_MARKER" -v end="$WEAVE_CODEX_END_MARKER" '
|
|
1767
|
+
$0 == begin { inblk = 1; next }
|
|
1768
|
+
$0 == end { inblk = 0; next }
|
|
1769
|
+
inblk && match($0, /"X-Weave-Router-Key"[[:space:]]*=[[:space:]]*"[^"]*"/) {
|
|
1770
|
+
hdr = substr($0, RSTART, RLENGTH)
|
|
1771
|
+
sub(/^.*=[[:space:]]*"/, "", hdr)
|
|
1772
|
+
sub(/"$/, "", hdr)
|
|
1773
|
+
print hdr
|
|
1774
|
+
exit
|
|
1775
|
+
}
|
|
1776
|
+
' "$f" 2>/dev/null || true
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
# read_opencode_key prints the router key out of opencode.json's managed
|
|
1780
|
+
# provider, or nothing. The header is the authoritative copy — options.apiKey is
|
|
1781
|
+
# only a parse-time placeholder for the @ai-sdk/openai provider.
|
|
1782
|
+
read_opencode_key() {
|
|
1783
|
+
local f="$1"
|
|
1784
|
+
key_source_is_own "$f" || return 0
|
|
1785
|
+
json_get "$f" '.provider.weave.options.headers["X-Weave-Router-Key"]'
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
# read_pi_key prints the router key this pi install already has. The dedicated
|
|
1789
|
+
# key file comes first — it is what the @workweave/router extension itself reads
|
|
1790
|
+
# at runtime — and models.json is the fallback for an install whose key file was
|
|
1791
|
+
# removed by hand.
|
|
1792
|
+
read_pi_key() {
|
|
1793
|
+
local key=""
|
|
1794
|
+
if key_source_is_own "$pi_key_file"; then
|
|
1795
|
+
key="$(tr -d '[:space:]' <"$pi_key_file" 2>/dev/null || true)"
|
|
1796
|
+
fi
|
|
1797
|
+
if [ -z "$key" ] && key_source_is_own "$pi_models_file"; then
|
|
1798
|
+
key="$(json_get "$pi_models_file" '.providers.weave.headers["X-Weave-Router-Key"]')"
|
|
1799
|
+
fi
|
|
1800
|
+
printf '%s' "$key"
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1667
1803
|
# read_installed_key prints the router key this install already has on disk, or
|
|
1668
|
-
# nothing
|
|
1669
|
-
#
|
|
1670
|
-
#
|
|
1804
|
+
# nothing, for whichever client is being installed. Reading it back is what makes
|
|
1805
|
+
# a re-run painless — see the token-handling section below. Defined up here
|
|
1806
|
+
# rather than with the rest of token handling because `models` dispatches (and
|
|
1807
|
+
# needs a key) before that section runs.
|
|
1671
1808
|
#
|
|
1672
|
-
# models_key_file_order echoes the precedence
|
|
1673
|
-
# to know *which* file the key came from can walk the same list (a
|
|
1674
|
-
# substitution around this function would discard any global it set).
|
|
1809
|
+
# models_key_file_order echoes the precedence the Claude reader uses, so a caller
|
|
1810
|
+
# that needs to know *which* file the key came from can walk the same list (a
|
|
1811
|
+
# command substitution around this function would discard any global it set).
|
|
1812
|
+
# models_config_file_for_target names the single managed config that holds both
|
|
1813
|
+
# the endpoint and the key for a non-Claude client. Endpoint and credential from
|
|
1814
|
+
# the same file are self-consistent, which is exactly the case
|
|
1815
|
+
# models_endpoint_is_trusted already treats as always-safe.
|
|
1816
|
+
models_config_file_for_target() {
|
|
1817
|
+
case "$target" in
|
|
1818
|
+
codex) printf '%s' "$codex_config_file" ;;
|
|
1819
|
+
opencode) printf '%s' "$opencode_config_file" ;;
|
|
1820
|
+
pi) printf '%s' "$pi_models_file" ;;
|
|
1821
|
+
esac
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1675
1824
|
models_key_file_order() {
|
|
1676
1825
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
1677
1826
|
printf '%s\n%s\n%s\n' "$local_settings_file" "$settings_file" "$settings_dir/.weave-parked.json"
|
|
@@ -1680,8 +1829,7 @@ models_key_file_order() {
|
|
|
1680
1829
|
fi
|
|
1681
1830
|
}
|
|
1682
1831
|
|
|
1683
|
-
|
|
1684
|
-
[ "$target" = "claude" ] || return 0
|
|
1832
|
+
read_claude_installed_key() {
|
|
1685
1833
|
local key="" candidate
|
|
1686
1834
|
# Mirror where the install path writes the key: project scope (no --dir) puts
|
|
1687
1835
|
# it in the gitignored settings.local.json, everything else inlines it into
|
|
@@ -1699,6 +1847,15 @@ read_installed_key() {
|
|
|
1699
1847
|
printf '%s' "$key"
|
|
1700
1848
|
}
|
|
1701
1849
|
|
|
1850
|
+
read_installed_key() {
|
|
1851
|
+
case "$target" in
|
|
1852
|
+
claude) read_claude_installed_key ;;
|
|
1853
|
+
codex) read_codex_key "$codex_config_file" ;;
|
|
1854
|
+
opencode) read_opencode_key "$opencode_config_file" ;;
|
|
1855
|
+
pi) read_pi_key ;;
|
|
1856
|
+
esac
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1702
1859
|
# resolve_installed_base_url prints the router endpoint this Claude Code install
|
|
1703
1860
|
# already points at, or nothing when none of its files carry a router-shaped
|
|
1704
1861
|
# one. The parked sidecar comes first: `off` moves the router URL there and
|
|
@@ -1724,6 +1881,54 @@ resolve_installed_base_url() {
|
|
|
1724
1881
|
printf ''
|
|
1725
1882
|
}
|
|
1726
1883
|
|
|
1884
|
+
# resolve_installed_endpoint prints the router endpoint this install already
|
|
1885
|
+
# points at, for whichever client is being installed, or nothing. `update` uses
|
|
1886
|
+
# it so a refresh never silently retargets a self-hosted install at the hosted
|
|
1887
|
+
# default.
|
|
1888
|
+
#
|
|
1889
|
+
# The stored shapes differ by client and are normalized back to the root the
|
|
1890
|
+
# installer takes on the command line: Codex and opencode hold an OpenAI-style
|
|
1891
|
+
# base ending in /v1, while pi holds the bare root (its Anthropic SDK appends
|
|
1892
|
+
# /v1/messages itself). Same tracked-file gate as the key readers — a config the
|
|
1893
|
+
# repo supplied is not this user's install, whichever field is being read.
|
|
1894
|
+
resolve_installed_endpoint() {
|
|
1895
|
+
local found=""
|
|
1896
|
+
case "$target" in
|
|
1897
|
+
claude)
|
|
1898
|
+
resolve_installed_base_url
|
|
1899
|
+
return 0
|
|
1900
|
+
;;
|
|
1901
|
+
codex)
|
|
1902
|
+
if key_source_is_own "$codex_config_file"; then
|
|
1903
|
+
found="$(awk -v begin="$WEAVE_CODEX_BEGIN_MARKER" -v end="$WEAVE_CODEX_END_MARKER" '
|
|
1904
|
+
$0 == begin { inblk = 1; next }
|
|
1905
|
+
$0 == end { inblk = 0; next }
|
|
1906
|
+
inblk && match($0, /^[[:space:]]*base_url[[:space:]]*=[[:space:]]*"[^"]*"/) {
|
|
1907
|
+
line = substr($0, RSTART, RLENGTH)
|
|
1908
|
+
sub(/^.*=[[:space:]]*"/, "", line)
|
|
1909
|
+
sub(/"$/, "", line)
|
|
1910
|
+
print line
|
|
1911
|
+
exit
|
|
1912
|
+
}
|
|
1913
|
+
' "$codex_config_file" 2>/dev/null || true)"
|
|
1914
|
+
fi
|
|
1915
|
+
found="${found%/v1}"
|
|
1916
|
+
;;
|
|
1917
|
+
opencode)
|
|
1918
|
+
if key_source_is_own "$opencode_config_file"; then
|
|
1919
|
+
found="$(json_get "$opencode_config_file" '.provider.weave.options.baseURL')"
|
|
1920
|
+
fi
|
|
1921
|
+
found="${found%/v1}"
|
|
1922
|
+
;;
|
|
1923
|
+
pi)
|
|
1924
|
+
if key_source_is_own "$pi_models_file"; then
|
|
1925
|
+
found="$(json_get "$pi_models_file" '.providers.weave.baseUrl')"
|
|
1926
|
+
fi
|
|
1927
|
+
;;
|
|
1928
|
+
esac
|
|
1929
|
+
printf '%s' "${found%/}"
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1727
1932
|
# models_endpoint_is_trusted returns 0 when it is safe to send the router key
|
|
1728
1933
|
# resolved from $2 to the endpoint resolved from $1.
|
|
1729
1934
|
#
|
|
@@ -1962,6 +2167,9 @@ toggle_codex() {
|
|
|
1962
2167
|
{print}
|
|
1963
2168
|
' "$f" >"$tmp" && mv "$tmp" "$f"
|
|
1964
2169
|
chmod 600 "$f"
|
|
2170
|
+
if [ -f "$codex_status_file" ] && grep -Fq '<!-- weave-router managed codex status -->' "$codex_status_file"; then
|
|
2171
|
+
"$codex_status_file" --off >/dev/null 2>&1 || true
|
|
2172
|
+
fi
|
|
1965
2173
|
ok "Codex is ${C_BOLD}off${C_RESET} (default provider). Takes effect on your next 'codex' run."
|
|
1966
2174
|
;;
|
|
1967
2175
|
on)
|
|
@@ -1977,6 +2185,9 @@ toggle_codex() {
|
|
|
1977
2185
|
{print}
|
|
1978
2186
|
' "$f" >"$tmp" && mv "$tmp" "$f"
|
|
1979
2187
|
chmod 600 "$f"
|
|
2188
|
+
if [ -f "$codex_status_file" ] && grep -Fq '<!-- weave-router managed codex status -->' "$codex_status_file"; then
|
|
2189
|
+
"$codex_status_file" --on >/dev/null 2>&1 || true
|
|
2190
|
+
fi
|
|
1980
2191
|
ok "Codex is ${C_BOLD}on${C_RESET} (routing through the Weave Router). Takes effect on your next 'codex' run."
|
|
1981
2192
|
;;
|
|
1982
2193
|
esac
|
|
@@ -2134,7 +2345,7 @@ models_fail() {
|
|
|
2134
2345
|
if [ -n "$detail" ]; then
|
|
2135
2346
|
err "$detail"
|
|
2136
2347
|
else
|
|
2137
|
-
err "The router rejected this installation's key. Re-run 'npx @workweave/router
|
|
2348
|
+
err "The router rejected this installation's key. Re-run 'npx @workweave/router --$target --rotate-key' to install a current one."
|
|
2138
2349
|
fi
|
|
2139
2350
|
;;
|
|
2140
2351
|
404)
|
|
@@ -2246,8 +2457,8 @@ models_list() {
|
|
|
2246
2457
|
fi
|
|
2247
2458
|
models_render_list "$models_http_body"
|
|
2248
2459
|
models_print_preferred
|
|
2249
|
-
printf "\n%sEnable a model:%s npx @workweave/router models enable <id>
|
|
2250
|
-
printf "%sDisable a model:%s npx @workweave/router models disable <id>
|
|
2460
|
+
printf "\n%sEnable a model:%s npx @workweave/router models enable <id> --%s\n" "$C_DIM" "$C_RESET" "$target"
|
|
2461
|
+
printf "%sDisable a model:%s npx @workweave/router models disable <id> --%s\n" "$C_DIM" "$C_RESET" "$target"
|
|
2251
2462
|
}
|
|
2252
2463
|
|
|
2253
2464
|
models_providers_list() {
|
|
@@ -2331,14 +2542,17 @@ models_prefer() {
|
|
|
2331
2542
|
}
|
|
2332
2543
|
|
|
2333
2544
|
models_usage() {
|
|
2545
|
+
# Echo back the client the caller actually named, so a Codex user is never
|
|
2546
|
+
# told to run a command that would target (or create) a Claude Code install.
|
|
2547
|
+
local c="--${target}"
|
|
2334
2548
|
err "$1"
|
|
2335
2549
|
printf '%s\n' \
|
|
2336
|
-
" npx @workweave/router models
|
|
2337
|
-
" npx @workweave/router models enable <id> [<id>…]
|
|
2338
|
-
" npx @workweave/router models disable <id> [<id>…]
|
|
2339
|
-
" npx @workweave/router models providers
|
|
2340
|
-
" npx @workweave/router models providers disable <name>
|
|
2341
|
-
" npx @workweave/router models prefer <id> [<id>…]
|
|
2550
|
+
" npx @workweave/router models $c # list models" \
|
|
2551
|
+
" npx @workweave/router models enable <id> [<id>…] $c" \
|
|
2552
|
+
" npx @workweave/router models disable <id> [<id>…] $c" \
|
|
2553
|
+
" npx @workweave/router models providers $c # list providers" \
|
|
2554
|
+
" npx @workweave/router models providers disable <name> $c" \
|
|
2555
|
+
" npx @workweave/router models prefer <id> [<id>…] $c # ranking ('clear' to drop)" >&2
|
|
2342
2556
|
exit 2
|
|
2343
2557
|
}
|
|
2344
2558
|
|
|
@@ -2394,23 +2608,30 @@ if [ "$mode" = "models" ]; then
|
|
|
2394
2608
|
# never the hosted defaults: a self-hosted user pointing at their own router
|
|
2395
2609
|
# would otherwise silently edit the hosted one's installation.
|
|
2396
2610
|
if [ "$base_url_explicit" != "true" ]; then
|
|
2397
|
-
models_base="$(
|
|
2611
|
+
models_base="$(resolve_installed_endpoint)"
|
|
2398
2612
|
if [ -z "$models_base" ]; then
|
|
2399
|
-
err "No Weave Router install found for
|
|
2613
|
+
err "No Weave Router install found for $target in this scope. Run 'npx @workweave/router --$target' first, or pass --base-url."
|
|
2400
2614
|
exit 1
|
|
2401
2615
|
fi
|
|
2402
2616
|
base_url="$models_base"
|
|
2403
|
-
#
|
|
2617
|
+
# resolve_installed_endpoint ran in a command substitution, so any global it
|
|
2404
2618
|
# set died with that subshell. Recover the source by walking the same
|
|
2405
|
-
# precedence to find which file holds the endpoint we just adopted.
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2619
|
+
# precedence to find which file holds the endpoint we just adopted. Only
|
|
2620
|
+
# Claude Code spreads its endpoint across several files; every other client
|
|
2621
|
+
# keeps endpoint and key in one managed config, which is self-consistent by
|
|
2622
|
+
# construction (see models_endpoint_is_trusted's same-file rule).
|
|
2623
|
+
if [ "$target" = "claude" ]; then
|
|
2624
|
+
while IFS= read -r models_src_candidate; do
|
|
2625
|
+
[ -n "$models_src_candidate" ] || continue
|
|
2626
|
+
models_src_url="$(json_get "$models_src_candidate" '.env.ANTHROPIC_BASE_URL')"
|
|
2627
|
+
if [ "${models_src_url%/}" = "$models_base" ]; then
|
|
2628
|
+
models_base_source="$models_src_candidate"
|
|
2629
|
+
break
|
|
2630
|
+
fi
|
|
2631
|
+
done <<<"$(models_base_file_order)"
|
|
2632
|
+
else
|
|
2633
|
+
models_base_source="$(models_config_file_for_target)"
|
|
2634
|
+
fi
|
|
2414
2635
|
fi
|
|
2415
2636
|
# WEAVE_ROUTER_KEY is the user's own choice, so it pairs with any endpoint.
|
|
2416
2637
|
if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
@@ -2421,16 +2642,20 @@ if [ "$mode" = "models" ]; then
|
|
|
2421
2642
|
# Same subshell caveat as the endpoint above: recover which file the key
|
|
2422
2643
|
# came from by re-reading them in read_installed_key's own precedence.
|
|
2423
2644
|
models_key_source=""
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2645
|
+
if [ "$target" = "claude" ]; then
|
|
2646
|
+
while IFS= read -r models_src_candidate; do
|
|
2647
|
+
[ -n "$models_src_candidate" ] || continue
|
|
2648
|
+
if [ -n "$(read_claude_key "$models_src_candidate")" ]; then
|
|
2649
|
+
models_key_source="$models_src_candidate"
|
|
2650
|
+
break
|
|
2651
|
+
fi
|
|
2652
|
+
done <<<"$(models_key_file_order)"
|
|
2653
|
+
else
|
|
2654
|
+
models_key_source="$(models_config_file_for_target)"
|
|
2655
|
+
fi
|
|
2431
2656
|
fi
|
|
2432
2657
|
if [ -z "$api_key" ]; then
|
|
2433
|
-
err "No router key found for
|
|
2658
|
+
err "No router key found for $target in this scope. Re-run 'npx @workweave/router --$target', or export WEAVE_ROUTER_KEY."
|
|
2434
2659
|
exit 1
|
|
2435
2660
|
fi
|
|
2436
2661
|
# Never send a key to an endpoint the checkout supplied. See
|
|
@@ -2472,8 +2697,8 @@ fi
|
|
|
2472
2697
|
# `off` parks the router URL and points Claude Code at Anthropic, so the parked
|
|
2473
2698
|
# sidecar is the authority while toggled off — reading the live file there would
|
|
2474
2699
|
# pin the install to api.anthropic.com.
|
|
2475
|
-
if [ "$mode" = "update" ] && [ "$base_url_explicit" != "true" ]
|
|
2476
|
-
installed_base="$(
|
|
2700
|
+
if [ "$mode" = "update" ] && [ "$base_url_explicit" != "true" ]; then
|
|
2701
|
+
installed_base="$(resolve_installed_endpoint)"
|
|
2477
2702
|
if [ -n "$installed_base" ]; then
|
|
2478
2703
|
base_url="${installed_base%/}"
|
|
2479
2704
|
fi
|
|
@@ -2745,63 +2970,29 @@ EOF
|
|
|
2745
2970
|
rmdir "$dst_dir" 2>/dev/null || true
|
|
2746
2971
|
}
|
|
2747
2972
|
|
|
2748
|
-
# Codex skills are the supported local extension surface for an action that
|
|
2749
|
-
# must edit Codex's configuration. A literal third-party slash command cannot
|
|
2750
|
-
# do this because Codex reserves `/…` for built-ins. The skill is invoked as
|
|
2751
|
-
# `$disable-routing`, asks Codex to run the existing safe toggle, and leaves
|
|
2752
|
-
# the managed router block in place for a later re-enable.
|
|
2753
|
-
install_codex_disable_routing_skill() {
|
|
2754
|
-
local skill_src=""
|
|
2755
|
-
local candidate dst_dir dst_file scope_args body
|
|
2756
|
-
for candidate in \
|
|
2757
|
-
"$script_dir/codex-skills/disable-routing/SKILL.md" \
|
|
2758
|
-
"$script_dir/../codex-skills/disable-routing/SKILL.md"
|
|
2759
|
-
do
|
|
2760
|
-
if [ -f "$candidate" ]; then
|
|
2761
|
-
skill_src="$candidate"
|
|
2762
|
-
break
|
|
2763
|
-
fi
|
|
2764
|
-
done
|
|
2765
|
-
[ -n "$skill_src" ] || { warn "Codex disable-routing skill template is missing; router configuration is still installed."; return 0; }
|
|
2766
|
-
grep -Fq "$WEAVE_CODEX_SKILL_MARKER" "$skill_src" \
|
|
2767
|
-
|| { warn "Codex disable-routing skill template has no ownership marker; leaving skills unchanged."; return 0; }
|
|
2768
|
-
|
|
2769
|
-
dst_dir="$codex_dir/skills/disable-routing"
|
|
2770
|
-
dst_file="$dst_dir/SKILL.md"
|
|
2771
|
-
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
2772
|
-
refuse_if_symlink "$codex_dir/skills"
|
|
2773
|
-
refuse_if_symlink "$dst_dir"
|
|
2774
|
-
refuse_if_symlink "$dst_file"
|
|
2775
|
-
elif [ -L "$codex_dir/skills" ] || [ -L "$dst_dir" ] || [ -L "$dst_file" ]; then
|
|
2776
|
-
warn "Codex disable-routing skill path contains a symlink; leaving it untouched."
|
|
2777
|
-
return 0
|
|
2778
|
-
fi
|
|
2779
|
-
if [ -e "$dst_file" ] && { [ ! -f "$dst_file" ] || ! grep -Fq "$WEAVE_CODEX_SKILL_MARKER" "$dst_file"; }; then
|
|
2780
|
-
warn "A user-owned Codex disable-routing skill already exists at $dst_file; leaving it untouched."
|
|
2781
|
-
return 0
|
|
2782
|
-
fi
|
|
2783
|
-
mkdir -p "$dst_dir"
|
|
2784
|
-
|
|
2785
|
-
scope_args=""
|
|
2786
|
-
if [ -n "$install_dir" ]; then
|
|
2787
|
-
scope_args=" --dir $(printf '%q' "$install_dir")"
|
|
2788
|
-
elif [ "$scope" = "project" ]; then
|
|
2789
|
-
scope_args=" --scope project"
|
|
2790
|
-
fi
|
|
2791
|
-
body="$(<"$skill_src")"
|
|
2792
|
-
body="${body//\{\{SCOPE\}\}/$scope_args}"
|
|
2793
|
-
printf '%s\n' "$body" >"$dst_file"
|
|
2794
|
-
ok "Codex skill installed: \$disable-routing"
|
|
2795
|
-
}
|
|
2796
2973
|
|
|
2797
|
-
# Install
|
|
2798
|
-
#
|
|
2799
|
-
#
|
|
2974
|
+
# Install every Codex-native skill the registry declares for this client:
|
|
2975
|
+
# prompt directives, their aliases, and local-config toggles like $router-off
|
|
2976
|
+
# that shell out to this installer's own verbs. weave_registry_skill_assets is
|
|
2977
|
+
# the union, canonical names plus aliases — Codex discovers skills by directory
|
|
2978
|
+
# name, so an advertised $fm needs its own installed skill rather than a
|
|
2979
|
+
# pointer to $force-model.
|
|
2980
|
+
#
|
|
2981
|
+
# A name with no SKILL.md is skipped: an alias may exist for the Claude command
|
|
2982
|
+
# surface without a Codex skill behind it (registry_test.sh pins which ones do).
|
|
2800
2983
|
install_codex_prompt_skills() {
|
|
2801
|
-
local canonical skill_src dst_dir dst_file scope_args body
|
|
2984
|
+
local canonical candidate skill_src dst_dir dst_file emit_src emit_dst scope_args body
|
|
2802
2985
|
while IFS= read -r canonical; do
|
|
2803
|
-
skill_src="
|
|
2804
|
-
|
|
2986
|
+
skill_src=""
|
|
2987
|
+
for candidate in \
|
|
2988
|
+
"$script_dir/codex-skills/$canonical/SKILL.md" \
|
|
2989
|
+
"$script_dir/../codex-skills/$canonical/SKILL.md"
|
|
2990
|
+
do
|
|
2991
|
+
[ -f "$candidate" ] || continue
|
|
2992
|
+
skill_src="$candidate"
|
|
2993
|
+
break
|
|
2994
|
+
done
|
|
2995
|
+
[ -n "$skill_src" ] || continue
|
|
2805
2996
|
grep -Fq "<!-- weave-router managed $canonical skill -->" "$skill_src" || continue
|
|
2806
2997
|
dst_dir="$codex_dir/skills/$canonical"
|
|
2807
2998
|
dst_file="$dst_dir/SKILL.md"
|
|
@@ -2824,18 +3015,480 @@ install_codex_prompt_skills() {
|
|
|
2824
3015
|
body="$(<"$skill_src")"
|
|
2825
3016
|
body="${body//\{\{SCOPE\}\}/$scope_args}"
|
|
2826
3017
|
printf '%s\n' "$body" >"$dst_file"
|
|
3018
|
+
# Prompt skills emit their directive through a script Codex execs; toggles
|
|
3019
|
+
# shell out to the installer's own verbs and ship none.
|
|
3020
|
+
emit_src="${skill_src%/SKILL.md}/scripts/emit.sh"
|
|
3021
|
+
if [ -f "$emit_src" ]; then
|
|
3022
|
+
mkdir -p "$dst_dir/scripts"
|
|
3023
|
+
emit_dst="$dst_dir/scripts/emit.sh"
|
|
3024
|
+
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
3025
|
+
refuse_if_symlink "$dst_dir/scripts"
|
|
3026
|
+
refuse_if_symlink "$emit_dst"
|
|
3027
|
+
elif [ -L "$dst_dir/scripts" ] || [ -L "$emit_dst" ]; then
|
|
3028
|
+
warn "Codex skill script path contains a symlink; leaving $canonical emit.sh untouched."
|
|
3029
|
+
else
|
|
3030
|
+
cp "$emit_src" "$emit_dst"
|
|
3031
|
+
chmod +x "$emit_dst"
|
|
3032
|
+
fi
|
|
3033
|
+
fi
|
|
2827
3034
|
ok "Codex skill installed: \$$canonical"
|
|
2828
3035
|
done <<EOF
|
|
2829
|
-
$(
|
|
3036
|
+
$(weave_registry_skill_assets codex)
|
|
2830
3037
|
EOF
|
|
2831
3038
|
}
|
|
2832
3039
|
|
|
3040
|
+
# ---------- post-install verification (shared by every target) ----------
|
|
3041
|
+
#
|
|
3042
|
+
# Every client gets the same two probes — reach the router, then prove the key
|
|
3043
|
+
# it was just handed actually authenticates — so a working install produces the
|
|
3044
|
+
# same green checks regardless of which one was installed.
|
|
3045
|
+
|
|
3046
|
+
# validate_key asks the router whether $api_key is live. The key goes over stdin
|
|
3047
|
+
# (`@-`) rather than a -H argument so it never appears in the process arg list,
|
|
3048
|
+
# where `ps` / /proc would expose it to other local users on a shared machine.
|
|
3049
|
+
# Wrapped in a function so the spinner's exec form sees a single command argv.
|
|
3050
|
+
validate_key() {
|
|
3051
|
+
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
3052
|
+
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
# rewrite_installed_key rewrites this target's managed config with the key in $1.
|
|
3056
|
+
# Used by the rejected-key fallback below to swap in a replacement without
|
|
3057
|
+
# re-running the whole install.
|
|
3058
|
+
rewrite_installed_key() {
|
|
3059
|
+
local key="$1"
|
|
3060
|
+
case "$target" in
|
|
3061
|
+
claude) write_claude_settings "$key" ;;
|
|
3062
|
+
codex) write_codex_config "$codex_config_file" "$base_url" "$key" "$user_email" "$user_name" ;;
|
|
3063
|
+
opencode) write_opencode_config "$opencode_config_file" "$base_url" "$key" "$user_email" "$user_name" ;;
|
|
3064
|
+
pi)
|
|
3065
|
+
write_pi_models_config "$pi_models_file" "$base_url" "$key" "$user_email" "$user_name"
|
|
3066
|
+
printf '%s\n' "$key" >"$pi_key_file"
|
|
3067
|
+
chmod 600 "$pi_key_file"
|
|
3068
|
+
;;
|
|
3069
|
+
esac
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
verify_install() {
|
|
3073
|
+
if [ "$quiet" != "true" ]; then
|
|
3074
|
+
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
3075
|
+
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
3076
|
+
fi
|
|
3077
|
+
fi
|
|
3078
|
+
|
|
3079
|
+
[ -n "$api_key" ] || return 0
|
|
3080
|
+
|
|
3081
|
+
spin "Validating API key" validate_key && return 0
|
|
3082
|
+
|
|
3083
|
+
# A key we read back off disk can have been revoked or rotated since it was
|
|
3084
|
+
# installed, and reusing it silently would leave a broken install behind
|
|
3085
|
+
# exactly where the old behavior (always prompt) would have fixed it. Ask
|
|
3086
|
+
# once, then rewrite the config with whatever the user pastes. Anywhere a
|
|
3087
|
+
# prompt isn't available — non-interactive, update, --quiet, no tty, or a
|
|
3088
|
+
# key that didn't come from disk — keep the historical warn-and-continue.
|
|
3089
|
+
if [ "$api_key_source" = "disk" ] && [ "$non_interactive" != "true" ] \
|
|
3090
|
+
&& [ "$quiet" != "true" ] && [ -r /dev/tty ]; then
|
|
3091
|
+
warn "The router key already installed was rejected (revoked or rotated)."
|
|
3092
|
+
prompt_for_key
|
|
3093
|
+
rewrite_installed_key "$api_key"
|
|
3094
|
+
if ! spin "Validating API key" validate_key; then
|
|
3095
|
+
warn "Router rejected the API key (check it matches the dashboard at $base_url)."
|
|
3096
|
+
fi
|
|
3097
|
+
return 0
|
|
3098
|
+
fi
|
|
3099
|
+
|
|
3100
|
+
if [ "$mode" = "update" ]; then
|
|
3101
|
+
# update is meant for cron/scripting: a rejected key is a real failure,
|
|
3102
|
+
# not a note in the log. --quiet callers opted out of the noise, not out
|
|
3103
|
+
# of the nonzero exit — a scheduled run that reports success here would
|
|
3104
|
+
# hide a revoked key indefinitely.
|
|
3105
|
+
if [ "$quiet" = "true" ]; then
|
|
3106
|
+
warn "Router rejected the installed API key. Re-run the installer to set a new one."
|
|
3107
|
+
else
|
|
3108
|
+
err "Router rejected the installed API key. Re-run the installer to set a new one."
|
|
3109
|
+
fi
|
|
3110
|
+
exit 1
|
|
3111
|
+
fi
|
|
3112
|
+
|
|
3113
|
+
warn "Router rejected the API key (check it matches the dashboard at $base_url)."
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
# announce_done prints the closing line for the client named in $1. `update`
|
|
3117
|
+
# refreshed an existing install rather than creating one, so it says so and
|
|
3118
|
+
# skips the uninstall hint — that run was never the user's first contact with
|
|
3119
|
+
# the installer.
|
|
3120
|
+
announce_done() {
|
|
3121
|
+
printf "\n"
|
|
3122
|
+
if [ "$mode" = "update" ]; then
|
|
3123
|
+
printf "%s✓%s %s%sWeave Router config refreshed for %s.%s\n" \
|
|
3124
|
+
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$1" "$C_RESET"
|
|
3125
|
+
return 0
|
|
3126
|
+
fi
|
|
3127
|
+
printf "%s✓%s %s%sWeave Router installed for %s.%s\n" \
|
|
3128
|
+
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$1" "$C_RESET"
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
# ---------- codex install path (dispatch + exit before the Claude-only writes) ----------
|
|
3132
|
+
|
|
3133
|
+
# Install the Codex lifecycle helper that reflects the active router and the
|
|
3134
|
+
# latest known routed model in the terminal title. The embedded copy keeps the
|
|
3135
|
+
# standalone curl installer feature-complete; packaged installs use the
|
|
3136
|
+
# canonical sibling asset.
|
|
3137
|
+
install_codex_status_script() {
|
|
3138
|
+
local candidate status_src=""
|
|
3139
|
+
for candidate in \
|
|
3140
|
+
"$script_dir/codex-status.sh" \
|
|
3141
|
+
"$script_dir/../codex-status.sh"
|
|
3142
|
+
do
|
|
3143
|
+
if [ -f "$candidate" ]; then
|
|
3144
|
+
status_src="$candidate"
|
|
3145
|
+
break
|
|
3146
|
+
fi
|
|
3147
|
+
done
|
|
3148
|
+
if [ "$scope" = "user" ] && [ -z "$install_dir" ]; then
|
|
3149
|
+
mkdir -p "$(dirname "$codex_status_file")"
|
|
3150
|
+
else
|
|
3151
|
+
refuse_if_symlink "$codex_status_file"
|
|
3152
|
+
fi
|
|
3153
|
+
if [ -e "$codex_status_file" ] && { [ ! -f "$codex_status_file" ] || ! grep -Fq '<!-- weave-router managed codex status -->' "$codex_status_file"; }; then
|
|
3154
|
+
warn "A user-owned Codex status helper already exists at $codex_status_file; leaving it untouched."
|
|
3155
|
+
return 1
|
|
3156
|
+
fi
|
|
3157
|
+
if [ -n "$status_src" ]; then
|
|
3158
|
+
grep -Fq '<!-- weave-router managed codex status -->' "$status_src" || {
|
|
3159
|
+
warn "Codex status helper has no ownership marker; leaving it unchanged."
|
|
3160
|
+
return 1
|
|
3161
|
+
}
|
|
3162
|
+
cp "$status_src" "$codex_status_file"
|
|
3163
|
+
else
|
|
3164
|
+
cat >"$codex_status_file" <<'CODEX_STATUS_EOF'
|
|
3165
|
+
#!/usr/bin/env bash
|
|
3166
|
+
# <!-- weave-router managed codex status -->
|
|
3167
|
+
#
|
|
3168
|
+
# Codex lifecycle hook for the Weave Router. Codex passes a JSON object on
|
|
3169
|
+
# stdin; the Stop hook includes the last assistant message, which carries the
|
|
3170
|
+
# router's routed-model marker when the selected model changes. The helper
|
|
3171
|
+
# keeps the last known routed model per session and reflects it in the terminal
|
|
3172
|
+
# title, so the active router remains visible between turns without injecting
|
|
3173
|
+
# another message into the conversation.
|
|
3174
|
+
#
|
|
3175
|
+
# Savings come from the router, not from local arithmetic. Codex records its
|
|
3176
|
+
# own requested model on every turn and never the served one, so the per-turn
|
|
3177
|
+
# pricing the Claude Code statusline does cannot be reproduced here — it would
|
|
3178
|
+
# price both sides of the comparison at the same model and report zero. The
|
|
3179
|
+
# router already sums the real thing per session, so the hook fetches
|
|
3180
|
+
# GET <base>/v1/sessions/<id>/cost and renders what it returns. The fetch runs
|
|
3181
|
+
# in a detached subshell writing a cache the NEXT turn reads, so no turn ever
|
|
3182
|
+
# blocks on the network, and every failure path leaves the title model-only.
|
|
3183
|
+
|
|
3184
|
+
set -euo pipefail
|
|
3185
|
+
|
|
3186
|
+
state_root="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router/codex"
|
|
3187
|
+
helper_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)"
|
|
3188
|
+
disabled_marker="$helper_dir/.weave-router-disabled"
|
|
3189
|
+
router_badge_sentinel=$''
|
|
3190
|
+
# Must stay verbatim in sync with install.sh / uninstall.sh: the endpoint read
|
|
3191
|
+
# below is scoped to this block so a key-shaped string elsewhere in the user's
|
|
3192
|
+
# config.toml is never adopted.
|
|
3193
|
+
codex_begin_marker="# >>> weave-router managed (do not edit between markers) >>>"
|
|
3194
|
+
codex_end_marker="# <<< weave-router managed <<<"
|
|
3195
|
+
|
|
3196
|
+
emit_title() {
|
|
3197
|
+
local title="$1"
|
|
3198
|
+
if [ -n "${WEAVE_CODEX_STATUS_TITLE_FILE:-}" ]; then
|
|
3199
|
+
printf '%s\n' "$title" >"$WEAVE_CODEX_STATUS_TITLE_FILE"
|
|
3200
|
+
elif [ -w /dev/tty ]; then
|
|
3201
|
+
printf '\033]0;%s\007' "$title" >/dev/tty
|
|
3202
|
+
fi
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
safe_session_id() {
|
|
3206
|
+
local id="$1"
|
|
3207
|
+
case "$id" in
|
|
3208
|
+
''|*[!A-Za-z0-9._-]*) return 1 ;;
|
|
3209
|
+
esac
|
|
3210
|
+
[ "${#id}" -le 128 ] || return 1
|
|
3211
|
+
printf '%s' "$id"
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
safe_display_value() {
|
|
3215
|
+
printf '%s' "$1" | sed 's/[^A-Za-z0-9._:\/-]//g' | cut -c1-128
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
state_file_for() {
|
|
3219
|
+
local id
|
|
3220
|
+
id="$(safe_session_id "$1")" || return 1
|
|
3221
|
+
printf '%s/%s.state' "$state_root" "$id"
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
read_state() {
|
|
3225
|
+
local file="$1" key value
|
|
3226
|
+
[ -f "$file" ] || return 0
|
|
3227
|
+
while IFS='=' read -r key value; do
|
|
3228
|
+
case "$key" in
|
|
3229
|
+
routed_model) routed_model="$value" ;;
|
|
3230
|
+
esac
|
|
3231
|
+
done <"$file"
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
write_state() {
|
|
3235
|
+
local file="$1" tmp
|
|
3236
|
+
mkdir -p "$state_root"
|
|
3237
|
+
chmod 700 "$state_root"
|
|
3238
|
+
tmp="$(mktemp "$state_root/.state.XXXXXX")"
|
|
3239
|
+
printf 'requested_model=%s\nrouted_model=%s\n' "$requested_model" "$routed_model" >"$tmp"
|
|
3240
|
+
chmod 600 "$tmp"
|
|
3241
|
+
mv "$tmp" "$file"
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
cost_file_for() {
|
|
3245
|
+
local id
|
|
3246
|
+
id="$(safe_session_id "$1")" || return 1
|
|
3247
|
+
printf '%s/%s.cost' "$state_root" "$id"
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
# Reads the router base URL and key out of the Codex config this install owns.
|
|
3251
|
+
# Resolved from the helper's own location first so a project-scope install never
|
|
3252
|
+
# reads (or leaks) the user-scope key: the project helper lives in the same
|
|
3253
|
+
# .codex directory as its config, while the user-scope helper sits in ~/.weave
|
|
3254
|
+
# and reads ~/.codex. Values are scoped to the managed block so a key-shaped
|
|
3255
|
+
# string the user wrote elsewhere in the file is never adopted. awk, not a TOML
|
|
3256
|
+
# parser, because the Codex target deliberately does not require jq for config
|
|
3257
|
+
# reads.
|
|
3258
|
+
read_codex_endpoint() {
|
|
3259
|
+
local config=""
|
|
3260
|
+
# Project/custom installs name the helper weave-status.sh and keep it next
|
|
3261
|
+
# to their config.toml. Falling through to ~/.codex would fetch another
|
|
3262
|
+
# installation's session cost with that key. The user-scope helper
|
|
3263
|
+
# (codex-status.sh in ~/.weave) has no adjacent config and owns HOME.
|
|
3264
|
+
if [ -f "$helper_dir/config.toml" ]; then
|
|
3265
|
+
config="$helper_dir/config.toml"
|
|
3266
|
+
elif [ "$(basename "$0")" != "weave-status.sh" ] && [ -f "$HOME/.codex/config.toml" ]; then
|
|
3267
|
+
config="$HOME/.codex/config.toml"
|
|
3268
|
+
fi
|
|
3269
|
+
[ -n "$config" ] || return 0
|
|
3270
|
+
awk -v begin="$codex_begin_marker" -v end="$codex_end_marker" '
|
|
3271
|
+
$0 == begin { inblk = 1; next }
|
|
3272
|
+
$0 == end { inblk = 0; next }
|
|
3273
|
+
!inblk { next }
|
|
3274
|
+
match($0, /base_url[[:space:]]*=[[:space:]]*"[^"]*"/) {
|
|
3275
|
+
v = substr($0, RSTART, RLENGTH)
|
|
3276
|
+
sub(/^.*=[[:space:]]*"/, "", v); sub(/"$/, "", v)
|
|
3277
|
+
url = v
|
|
3278
|
+
}
|
|
3279
|
+
match($0, /"X-Weave-Router-Key"[[:space:]]*=[[:space:]]*"[^"]*"/) {
|
|
3280
|
+
v = substr($0, RSTART, RLENGTH)
|
|
3281
|
+
sub(/^.*=[[:space:]]*"/, "", v); sub(/"$/, "", v)
|
|
3282
|
+
key = v
|
|
3283
|
+
}
|
|
3284
|
+
END { if (url != "" && key != "") printf "%s\n%s\n", url, key }
|
|
3285
|
+
' "$config" 2>/dev/null || true
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
# Kicks off a detached fetch of this session's committed cost. The result lands
|
|
3289
|
+
# in a cache the next turn reads; this turn renders whatever is already there.
|
|
3290
|
+
# Fire-and-forget on purpose — a slow or unreachable router must never stall a
|
|
3291
|
+
# Codex turn, and every failure simply leaves the previous cache in place.
|
|
3292
|
+
refresh_session_cost() {
|
|
3293
|
+
local id="$1" file="$2"
|
|
3294
|
+
[ "${WEAVE_CODEX_STATUS_SAVINGS:-1}" = "0" ] && return 0
|
|
3295
|
+
command -v curl >/dev/null 2>&1 || return 0
|
|
3296
|
+
|
|
3297
|
+
local endpoint base_url key
|
|
3298
|
+
endpoint="$(read_codex_endpoint)" || return 0
|
|
3299
|
+
base_url="$(printf '%s' "$endpoint" | sed -n 1p)"
|
|
3300
|
+
key="$(printf '%s' "$endpoint" | sed -n 2p)"
|
|
3301
|
+
[ -n "$base_url" ] && [ -n "$key" ] || return 0
|
|
3302
|
+
|
|
3303
|
+
mkdir -p "$state_root" 2>/dev/null || return 0
|
|
3304
|
+
chmod 700 "$state_root" 2>/dev/null || true
|
|
3305
|
+
|
|
3306
|
+
(
|
|
3307
|
+
exec </dev/null
|
|
3308
|
+
# mkdir is the portable atomic test-and-set. A crashed holder would block
|
|
3309
|
+
# refreshes forever, so a lock older than the fetch timeout is reclaimed.
|
|
3310
|
+
lock="$file.lock"
|
|
3311
|
+
if ! mkdir "$lock" 2>/dev/null; then
|
|
3312
|
+
lock_mtime="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)" || lock_mtime=0
|
|
3313
|
+
lock_now="$(date +%s 2>/dev/null)" || lock_now=0
|
|
3314
|
+
if [ "${lock_mtime:-0}" -le 0 ] || [ $(( lock_now - lock_mtime )) -le 30 ]; then
|
|
3315
|
+
exit 0
|
|
3316
|
+
fi
|
|
3317
|
+
rm -rf "$lock" 2>/dev/null
|
|
3318
|
+
mkdir "$lock" 2>/dev/null || exit 0
|
|
3319
|
+
fi
|
|
3320
|
+
trap 'rmdir "$lock" 2>/dev/null' EXIT
|
|
3321
|
+
|
|
3322
|
+
# A file:// base is the offline/test seam: curl reads it as the response
|
|
3323
|
+
# body directly, so the endpoint path is meaningless for it.
|
|
3324
|
+
url="${base_url%/}"
|
|
3325
|
+
case "$url" in
|
|
3326
|
+
file://*) ;;
|
|
3327
|
+
*) url="${url%/v1}/v1/sessions/$id/cost" ;;
|
|
3328
|
+
esac
|
|
3329
|
+
body="$(curl -fsS --max-time 5 -H "X-Weave-Router-Key: $key" "$url" 2>/dev/null)" || exit 0
|
|
3330
|
+
# savings_usd is the router's own (requested - actual). A body without it
|
|
3331
|
+
# (404, error envelope, older router) writes nothing and leaves the cache.
|
|
3332
|
+
savings="$(printf '%s' "$body" | jq -r '.savings_usd // empty' 2>/dev/null)" || exit 0
|
|
3333
|
+
case "$savings" in
|
|
3334
|
+
''|*[!0-9.eE+-]*) exit 0 ;;
|
|
3335
|
+
esac
|
|
3336
|
+
tmp="$file.tmp.$$"
|
|
3337
|
+
mkdir -p "$(dirname "$file")" 2>/dev/null
|
|
3338
|
+
if printf '%s' "$savings" >"$tmp" 2>/dev/null; then
|
|
3339
|
+
chmod 600 "$tmp" 2>/dev/null
|
|
3340
|
+
mv "$tmp" "$file" 2>/dev/null
|
|
3341
|
+
fi
|
|
3342
|
+
rm -f "$tmp" 2>/dev/null
|
|
3343
|
+
) >/dev/null 2>&1 &
|
|
3344
|
+
disown 2>/dev/null || true
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
# Renders the cached savings as a display clause, or nothing. Values below a
|
|
3348
|
+
# cent read as "<$0.01" rather than "$0.00", which would be indistinguishable
|
|
3349
|
+
# from "the router ran and did not beat your selection". Negative totals are
|
|
3350
|
+
# omitted entirely: the router picked a pricier model for quality on this
|
|
3351
|
+
# session and "saved -$0.02" is a worse answer than staying quiet.
|
|
3352
|
+
savings_clause() {
|
|
3353
|
+
local file="$1" raw
|
|
3354
|
+
[ "${WEAVE_CODEX_STATUS_SAVINGS:-1}" = "0" ] && return 0
|
|
3355
|
+
[ -f "$file" ] || return 0
|
|
3356
|
+
raw="$(cat "$file" 2>/dev/null)" || return 0
|
|
3357
|
+
case "$raw" in
|
|
3358
|
+
''|*[!0-9.eE+-]*) return 0 ;;
|
|
3359
|
+
esac
|
|
3360
|
+
awk -v v="$raw" 'BEGIN{
|
|
3361
|
+
v = v + 0
|
|
3362
|
+
if (v < 0.005) { exit }
|
|
3363
|
+
if (v < 0.01) { printf " · saved <$0.01"; exit }
|
|
3364
|
+
printf " · saved $%.2f", v
|
|
3365
|
+
}' 2>/dev/null || true
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
set -e
|
|
3369
|
+
|
|
3370
|
+
case "${1:-hook}" in
|
|
3371
|
+
--direct)
|
|
3372
|
+
emit_title "Codex · direct"
|
|
3373
|
+
exit 0
|
|
3374
|
+
;;
|
|
3375
|
+
--on)
|
|
3376
|
+
rm -f "$disabled_marker"
|
|
3377
|
+
emit_title "Weave Router · active"
|
|
3378
|
+
exit 0
|
|
3379
|
+
;;
|
|
3380
|
+
--off)
|
|
3381
|
+
[ ! -L "$disabled_marker" ] || exit 0
|
|
3382
|
+
: >"$disabled_marker"
|
|
3383
|
+
chmod 600 "$disabled_marker"
|
|
3384
|
+
emit_title "Codex · direct"
|
|
3385
|
+
exit 0
|
|
3386
|
+
;;
|
|
3387
|
+
esac
|
|
3388
|
+
|
|
3389
|
+
payload="$(cat)"
|
|
3390
|
+
[ -n "$payload" ] || exit 0
|
|
3391
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
3392
|
+
jq -e . >/dev/null 2>&1 <<<"$payload" || exit 0
|
|
3393
|
+
|
|
3394
|
+
hook_event_name="$(jq -r '.hook_event_name // ""' <<<"$payload")"
|
|
3395
|
+
if [ "$hook_event_name" = "SessionStart" ]; then
|
|
3396
|
+
if [ -f "$disabled_marker" ]; then
|
|
3397
|
+
emit_title "Codex · direct"
|
|
3398
|
+
jq -cn '{systemMessage:"Codex direct · Weave Router is off"}'
|
|
3399
|
+
else
|
|
3400
|
+
emit_title "Weave Router · active"
|
|
3401
|
+
jq -cn '{systemMessage:"Weave Router active · routed model appears in the terminal title"}'
|
|
3402
|
+
fi
|
|
3403
|
+
exit 0
|
|
3404
|
+
fi
|
|
3405
|
+
|
|
3406
|
+
if [ -f "$disabled_marker" ]; then
|
|
3407
|
+
exit 0
|
|
3408
|
+
fi
|
|
3409
|
+
|
|
3410
|
+
requested_model="$(safe_display_value "$(jq -r '.model // ""' <<<"$payload")")"
|
|
3411
|
+
routed_model=""
|
|
3412
|
+
session_id="$(jq -r '.session_id // ""' <<<"$payload")"
|
|
3413
|
+
last_assistant_message="$(jq -r '.last_assistant_message // ""' <<<"$payload")"
|
|
3414
|
+
|
|
3415
|
+
file=""
|
|
3416
|
+
if file="$(state_file_for "$session_id" 2>/dev/null)"; then
|
|
3417
|
+
read_state "$file"
|
|
3418
|
+
fi
|
|
3419
|
+
|
|
3420
|
+
# The marker is intentionally matched only at the beginning of the assistant
|
|
3421
|
+
# message. Do not treat ordinary prose that mentions the heading as metadata.
|
|
3422
|
+
first_line="${last_assistant_message%%$'\n'*}"
|
|
3423
|
+
marker_model=""
|
|
3424
|
+
force_model=""
|
|
3425
|
+
case "$first_line" in
|
|
3426
|
+
"${router_badge_sentinel}✦ **Weave Router** → "*)
|
|
3427
|
+
marker_model="${first_line#"${router_badge_sentinel}✦ **Weave Router** → "}"
|
|
3428
|
+
marker_model="${marker_model%% ·*}"
|
|
3429
|
+
;;
|
|
3430
|
+
"✦ **Weave Router** → "*)
|
|
3431
|
+
marker_model="${first_line#"✦ **Weave Router** → "}"
|
|
3432
|
+
marker_model="${marker_model%% ·*}"
|
|
3433
|
+
;;
|
|
3434
|
+
esac
|
|
3435
|
+
case "$first_line" in
|
|
3436
|
+
"Weave Router: force-model applied: "*" ("*)
|
|
3437
|
+
force_model="${first_line#"Weave Router: force-model applied: "}"
|
|
3438
|
+
force_model="${force_model%% (*}"
|
|
3439
|
+
;;
|
|
3440
|
+
esac
|
|
3441
|
+
if [ -n "$marker_model" ]; then
|
|
3442
|
+
routed_model="$(safe_display_value "$marker_model")"
|
|
3443
|
+
elif [ -n "$force_model" ]; then
|
|
3444
|
+
routed_model="$(safe_display_value "$force_model")"
|
|
3445
|
+
else
|
|
3446
|
+
routed_model="$(safe_display_value "$routed_model")"
|
|
3447
|
+
fi
|
|
3448
|
+
|
|
3449
|
+
if [ -n "$file" ]; then
|
|
3450
|
+
write_state "$file"
|
|
3451
|
+
fi
|
|
3452
|
+
|
|
3453
|
+
# The cache holds the previous turn's fetch; the refresh below serves the next
|
|
3454
|
+
# one. Router telemetry is written asynchronously anyway, so a just-finished
|
|
3455
|
+
# turn would not be included even in a blocking read — reading first and
|
|
3456
|
+
# refreshing after costs a turn of freshness and buys never blocking Codex.
|
|
3457
|
+
savings=""
|
|
3458
|
+
cost_file=""
|
|
3459
|
+
if cost_file="$(cost_file_for "$session_id" 2>/dev/null)"; then
|
|
3460
|
+
savings="$(savings_clause "$cost_file")"
|
|
3461
|
+
refresh_session_cost "$(safe_session_id "$session_id")" "$cost_file"
|
|
3462
|
+
fi
|
|
3463
|
+
|
|
3464
|
+
if [ -n "$routed_model" ] && [ -n "$requested_model" ] && [ "$routed_model" != "$requested_model" ]; then
|
|
3465
|
+
title="Weave Router · $routed_model ← $requested_model$savings"
|
|
3466
|
+
elif [ -n "$routed_model" ]; then
|
|
3467
|
+
title="Weave Router · $routed_model$savings"
|
|
3468
|
+
elif [ -n "$requested_model" ]; then
|
|
3469
|
+
title="Weave Router · active ← $requested_model$savings"
|
|
3470
|
+
else
|
|
3471
|
+
title="Weave Router · active$savings"
|
|
3472
|
+
fi
|
|
3473
|
+
emit_title "$title"
|
|
3474
|
+
if [ -n "$marker_model" ] || [ -n "$force_model" ]; then
|
|
3475
|
+
printf '%s' "$title" | jq -Rc '{systemMessage: .}'
|
|
3476
|
+
fi
|
|
3477
|
+
CODEX_STATUS_EOF
|
|
3478
|
+
fi
|
|
3479
|
+
chmod 700 "$codex_status_file"
|
|
3480
|
+
"$codex_status_file" --on >/dev/null 2>&1 || true
|
|
3481
|
+
ok "Codex status integration installed at $codex_status_file"
|
|
3482
|
+
}
|
|
2833
3483
|
|
|
2834
3484
|
if [ "$target" = "codex" ]; then
|
|
3485
|
+
if ! install_codex_status_script; then
|
|
3486
|
+
err "Cannot install the Codex status helper safely; refusing to write hooks that could execute unowned code."
|
|
3487
|
+
exit 1
|
|
3488
|
+
fi
|
|
2835
3489
|
write_codex_config "$codex_config_file" "$base_url" "$api_key" "$user_email" "$user_name"
|
|
2836
3490
|
ok "Codex config written to $codex_config_file"
|
|
2837
3491
|
remove_obsolete_codex_prompt_wrappers "$codex_dir/prompts"
|
|
2838
|
-
install_codex_disable_routing_skill
|
|
2839
3492
|
install_codex_prompt_skills
|
|
2840
3493
|
info "Codex router directives: begin the message with one space, e.g. ' /force-model gpt-5.6-terra'."
|
|
2841
3494
|
|
|
@@ -2847,44 +3500,26 @@ if [ "$target" = "codex" ]; then
|
|
|
2847
3500
|
gitignore="$git_root/.gitignore"
|
|
2848
3501
|
refuse_if_symlink "$gitignore"
|
|
2849
3502
|
for entry in \
|
|
2850
|
-
".codex/config.toml"
|
|
3503
|
+
".codex/config.toml" \
|
|
3504
|
+
".codex/weave-status.sh" \
|
|
3505
|
+
".codex/.weave-router-disabled"
|
|
2851
3506
|
do
|
|
2852
3507
|
if [ ! -f "$gitignore" ] || ! grep -qxF "$entry" "$gitignore"; then
|
|
2853
3508
|
printf '%s\n' "$entry" >>"$gitignore"
|
|
2854
3509
|
fi
|
|
2855
3510
|
done
|
|
2856
|
-
ok "Updated $gitignore (ignored
|
|
2857
|
-
fi
|
|
2858
|
-
|
|
2859
|
-
# Post-install verification: same probes the Claude path runs so a working
|
|
2860
|
-
# install gives the same green checks regardless of target.
|
|
2861
|
-
if [ "$quiet" != "true" ]; then
|
|
2862
|
-
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
2863
|
-
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
2864
|
-
fi
|
|
3511
|
+
ok "Updated $gitignore (ignored Codex router config and status helper)"
|
|
2865
3512
|
fi
|
|
2866
3513
|
|
|
2867
|
-
|
|
2868
|
-
# Pass the router key via stdin (`@-`) instead of -H so it never lands in
|
|
2869
|
-
# the process arg list. Mirrors the Claude-path validate logic.
|
|
2870
|
-
validate_codex_key() {
|
|
2871
|
-
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
2872
|
-
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
2873
|
-
}
|
|
2874
|
-
if ! spin "Validating API key" validate_codex_key; then
|
|
2875
|
-
warn "Router rejected the API key (check it matches the dashboard at $base_url/ui/)."
|
|
2876
|
-
fi
|
|
2877
|
-
fi
|
|
3514
|
+
verify_install
|
|
2878
3515
|
|
|
2879
|
-
|
|
2880
|
-
printf "%s✓%s %s%sWeave Router installed for Codex.%s\n" \
|
|
2881
|
-
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
3516
|
+
announce_done "Codex"
|
|
2882
3517
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
2883
3518
|
# Codex auto-discovers ~/.codex; for non-user installs the caller has to
|
|
2884
3519
|
# point CODEX_HOME at the directory we wrote so Codex finds our config.
|
|
2885
3520
|
info "Run Codex with CODEX_HOME=$codex_dir codex so it picks up this config."
|
|
2886
3521
|
fi
|
|
2887
|
-
print_uninstall_hint
|
|
3522
|
+
[ "$mode" = "update" ] || print_uninstall_hint
|
|
2888
3523
|
exit 0
|
|
2889
3524
|
fi
|
|
2890
3525
|
|
|
@@ -2932,26 +3567,9 @@ if [ "$target" = "opencode" ]; then
|
|
|
2932
3567
|
info "opencode restart required for commands to take effect."
|
|
2933
3568
|
fi
|
|
2934
3569
|
|
|
2935
|
-
|
|
2936
|
-
if [ "$quiet" != "true" ]; then
|
|
2937
|
-
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
2938
|
-
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
2939
|
-
fi
|
|
2940
|
-
fi
|
|
2941
|
-
|
|
2942
|
-
if [ -n "$api_key" ]; then
|
|
2943
|
-
validate_opencode_key() {
|
|
2944
|
-
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
2945
|
-
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
2946
|
-
}
|
|
2947
|
-
if ! spin "Validating API key" validate_opencode_key; then
|
|
2948
|
-
warn "Router rejected the API key (check it matches the dashboard at $base_url/ui/)."
|
|
2949
|
-
fi
|
|
2950
|
-
fi
|
|
3570
|
+
verify_install
|
|
2951
3571
|
|
|
2952
|
-
|
|
2953
|
-
printf "%s✓%s %s%sWeave Router installed for opencode.%s\n" \
|
|
2954
|
-
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
3572
|
+
announce_done "opencode"
|
|
2955
3573
|
# Surface the optional subscription-routing path only when this run actually
|
|
2956
3574
|
# registered the weave-claude login provider (which is written together with
|
|
2957
3575
|
# the plugin). Gate on its presence in the written config (authoritative)
|
|
@@ -2966,7 +3584,7 @@ if [ "$target" = "opencode" ]; then
|
|
|
2966
3584
|
# has to point opencode at the file explicitly.
|
|
2967
3585
|
info "Run opencode with OPENCODE_CONFIG=$opencode_config_file opencode."
|
|
2968
3586
|
fi
|
|
2969
|
-
print_uninstall_hint
|
|
3587
|
+
[ "$mode" = "update" ] || print_uninstall_hint
|
|
2970
3588
|
exit 0
|
|
2971
3589
|
fi
|
|
2972
3590
|
|
|
@@ -3007,30 +3625,13 @@ if [ "$target" = "pi" ]; then
|
|
|
3007
3625
|
ok "Updated $gitignore (ignored repo-local .pi router config)"
|
|
3008
3626
|
fi
|
|
3009
3627
|
|
|
3010
|
-
|
|
3011
|
-
if [ "$quiet" != "true" ]; then
|
|
3012
|
-
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
3013
|
-
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
3014
|
-
fi
|
|
3015
|
-
fi
|
|
3016
|
-
|
|
3017
|
-
if [ -n "$api_key" ]; then
|
|
3018
|
-
validate_pi_key() {
|
|
3019
|
-
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
3020
|
-
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
3021
|
-
}
|
|
3022
|
-
if ! spin "Validating API key" validate_pi_key; then
|
|
3023
|
-
warn "Router rejected the API key (check it matches the dashboard at $base_url/ui/)."
|
|
3024
|
-
fi
|
|
3025
|
-
fi
|
|
3628
|
+
verify_install
|
|
3026
3629
|
|
|
3027
3630
|
if [ -n "$lsp_langs" ]; then
|
|
3028
3631
|
install_lsp_servers "$lsp_langs"
|
|
3029
3632
|
fi
|
|
3030
3633
|
|
|
3031
|
-
|
|
3032
|
-
printf "%s✓%s %s%sWeave Router installed for pi.%s\n" \
|
|
3033
|
-
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
3634
|
+
announce_done "pi"
|
|
3034
3635
|
# Billing note: pi normally draws on a Claude subscription (OAuth); routing
|
|
3035
3636
|
# through the router switches to per-token billing on the router deployment
|
|
3036
3637
|
# key (or BYOK). Surface it at install so the change isn't a surprise.
|
|
@@ -3038,7 +3639,7 @@ if [ "$target" = "pi" ]; then
|
|
|
3038
3639
|
if [ "$scope" = "project" ] || [ -n "$install_dir" ]; then
|
|
3039
3640
|
info "Run pi with PI_CODING_AGENT_DIR=$pi_dir pi so it picks up this config."
|
|
3040
3641
|
fi
|
|
3041
|
-
print_uninstall_hint
|
|
3642
|
+
[ "$mode" = "update" ] || print_uninstall_hint
|
|
3042
3643
|
exit 0
|
|
3043
3644
|
fi
|
|
3044
3645
|
|
|
@@ -3063,9 +3664,9 @@ cat > "$statusline_file" << 'STATUSLINE_EOF'
|
|
|
3063
3664
|
# Renders:
|
|
3064
3665
|
# WEAVE ROUTER — claude-sonnet-4-5 ← claude-opus-4-7 · saved $1.23 · 12.4k in / 3.1k out / 45.2k cached
|
|
3065
3666
|
#
|
|
3066
|
-
# Pricing source of truth: router/
|
|
3067
|
-
#
|
|
3068
|
-
#
|
|
3667
|
+
# Pricing source of truth: internal/router/catalog. Input/output prices and
|
|
3668
|
+
# cache-read multipliers are generated by cmd/genprices. Cache creation remains
|
|
3669
|
+
# at 1.25× input pending TTL-aware pricing.
|
|
3069
3670
|
|
|
3070
3671
|
set -euo pipefail
|
|
3071
3672
|
|
|
@@ -3577,6 +4178,7 @@ prices='{
|
|
|
3577
4178
|
"z-ai/glm-5": 0.001,
|
|
3578
4179
|
"z-ai/glm-5.1": 0.0014,
|
|
3579
4180
|
"z-ai/glm-5.2": 0.0014,
|
|
4181
|
+
"z-ai/glm-5.3": 0.0014,
|
|
3580
4182
|
"z-ai/glm-5.3-flash": 0.00015
|
|
3581
4183
|
},
|
|
3582
4184
|
"output": {
|
|
@@ -3653,7 +4255,85 @@ prices='{
|
|
|
3653
4255
|
"z-ai/glm-5": 0.0032,
|
|
3654
4256
|
"z-ai/glm-5.1": 0.0044,
|
|
3655
4257
|
"z-ai/glm-5.2": 0.0044,
|
|
4258
|
+
"z-ai/glm-5.3": 0.0044,
|
|
3656
4259
|
"z-ai/glm-5.3-flash": 0.0005
|
|
4260
|
+
},
|
|
4261
|
+
"cache_read": {
|
|
4262
|
+
"claude-fable-5": 0.1,
|
|
4263
|
+
"claude-haiku-4-5": 0.1,
|
|
4264
|
+
"claude-opus-4-0": 0.1,
|
|
4265
|
+
"claude-opus-4-1": 0.1,
|
|
4266
|
+
"claude-opus-4-5": 0.1,
|
|
4267
|
+
"claude-opus-4-6": 0.1,
|
|
4268
|
+
"claude-opus-4-7": 0.1,
|
|
4269
|
+
"claude-opus-4-8": 0.1,
|
|
4270
|
+
"claude-opus-5": 0.1,
|
|
4271
|
+
"claude-sonnet-4-5": 0.1,
|
|
4272
|
+
"claude-sonnet-4-6": 0.1,
|
|
4273
|
+
"claude-sonnet-5": 0.1,
|
|
4274
|
+
"deepseek/deepseek-v4-flash": 0.2,
|
|
4275
|
+
"deepseek/deepseek-v4-pro": 0.11494252873563218,
|
|
4276
|
+
"deepseek/deepseek-v4-pro-0813": 0.11494252873563218,
|
|
4277
|
+
"gemini-2.0-flash": 0.25,
|
|
4278
|
+
"gemini-2.0-flash-lite": 0.25,
|
|
4279
|
+
"gemini-2.5-flash": 0.1,
|
|
4280
|
+
"gemini-2.5-flash-lite": 0.1,
|
|
4281
|
+
"gemini-2.5-pro": 0.1,
|
|
4282
|
+
"gemini-3-flash-preview": 0.1,
|
|
4283
|
+
"gemini-3-pro-preview": 0.1,
|
|
4284
|
+
"gemini-3.1-flash-lite-preview": 0.1,
|
|
4285
|
+
"gemini-3.1-pro-preview": 0.1,
|
|
4286
|
+
"gemini-3.5-flash": 0.1,
|
|
4287
|
+
"gemini-3.5-flash-lite": 0.1,
|
|
4288
|
+
"gemini-3.6-flash": 0.1,
|
|
4289
|
+
"gemini-3.7-flash": 0.1,
|
|
4290
|
+
"google/gemma-4-26b-a4b-it": 0.1,
|
|
4291
|
+
"gpt-4.1": 0.25,
|
|
4292
|
+
"gpt-4.1-mini": 0.25,
|
|
4293
|
+
"gpt-4.1-nano": 0.25,
|
|
4294
|
+
"gpt-4o": 0.5,
|
|
4295
|
+
"gpt-4o-mini": 0.5,
|
|
4296
|
+
"gpt-5": 0.1,
|
|
4297
|
+
"gpt-5-chat": 0.1,
|
|
4298
|
+
"gpt-5-mini": 0.1,
|
|
4299
|
+
"gpt-5-nano": 0.1,
|
|
4300
|
+
"gpt-5.4": 0.1,
|
|
4301
|
+
"gpt-5.4-mini": 0.1,
|
|
4302
|
+
"gpt-5.4-nano": 0.1,
|
|
4303
|
+
"gpt-5.4-pro": 1,
|
|
4304
|
+
"gpt-5.5": 0.1,
|
|
4305
|
+
"gpt-5.5-mini": 0.1,
|
|
4306
|
+
"gpt-5.5-nano": 0.1,
|
|
4307
|
+
"gpt-5.5-pro": 1,
|
|
4308
|
+
"gpt-5.6-luna": 0.1,
|
|
4309
|
+
"gpt-5.6-luna-pro": 0.1,
|
|
4310
|
+
"gpt-5.6-sol": 0.1,
|
|
4311
|
+
"gpt-5.6-sol-pro": 0.1,
|
|
4312
|
+
"gpt-5.6-terra": 0.1,
|
|
4313
|
+
"grok-4.5": 0.25,
|
|
4314
|
+
"grok-4.6": 0.25,
|
|
4315
|
+
"minimax/minimax-m2.7": 0.2,
|
|
4316
|
+
"minimax/minimax-m3": 0.2,
|
|
4317
|
+
"mistralai/mistral-small-2603": 0.1,
|
|
4318
|
+
"moonshotai/kimi-k2.5": 0.5,
|
|
4319
|
+
"moonshotai/kimi-k2.6": 0.1684,
|
|
4320
|
+
"moonshotai/kimi-k2.7": 0.2,
|
|
4321
|
+
"moonshotai/kimi-k3": 0.1,
|
|
4322
|
+
"qwen/qwen3-235b-a22b-2507": 0.5,
|
|
4323
|
+
"qwen/qwen3-30b-a3b-instruct-2507": 0.1684,
|
|
4324
|
+
"qwen/qwen3-coder": 0.1684,
|
|
4325
|
+
"qwen/qwen3-coder-next": 0.5,
|
|
4326
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.5,
|
|
4327
|
+
"qwen/qwen3.5-flash-02-23": 0.1,
|
|
4328
|
+
"qwen/qwen3.6-35b-a3b": 0.1,
|
|
4329
|
+
"qwen/qwen3.7-plus": 0.2,
|
|
4330
|
+
"qwen/qwen3.8-max": 0.125,
|
|
4331
|
+
"xiaomi/mimo-v2.5-pro": 0.1,
|
|
4332
|
+
"z-ai/glm-5": 0.2,
|
|
4333
|
+
"z-ai/glm-5.1": 0.18571428571428572,
|
|
4334
|
+
"z-ai/glm-5.2": 0.18571428571428572,
|
|
4335
|
+
"z-ai/glm-5.3": 0.18571428571428572,
|
|
4336
|
+
"z-ai/glm-5.3-flash": 0.2
|
|
3657
4337
|
}
|
|
3658
4338
|
}'
|
|
3659
4339
|
# END_GENERATED_PRICES
|
|
@@ -3738,12 +4418,9 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
3738
4418
|
# Compute a session running total: savings across every assistant turn
|
|
3739
4419
|
# whose marker reports a requested ≠ routed swap, plus cumulative token
|
|
3740
4420
|
# counts across every assistant turn (rerouted or not — total work the
|
|
3741
|
-
# session has done). cache_creation is priced at 1.25× input
|
|
3742
|
-
#
|
|
3743
|
-
#
|
|
3744
|
-
# in the savings comparison: both costs apply the same 0.1× weight to
|
|
3745
|
-
# cache_read_input_tokens, so the delta reflects the model-price
|
|
3746
|
-
# difference on the cached portion as well.
|
|
4421
|
+
# session has done). cache_creation is priced at 1.25× input; cache_read
|
|
4422
|
+
# uses each model's generated catalog multiplier. Both are no-ops when the
|
|
4423
|
+
# provider does not return those fields.
|
|
3747
4424
|
#
|
|
3748
4425
|
# The marker regex tolerates the optional "(<provider>)" segment and a
|
|
3749
4426
|
# `[1m]` / `-YYYYMMDD` suffix on either model name so transcripts written
|
|
@@ -3776,14 +4453,17 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
3776
4453
|
} as $t |
|
|
3777
4454
|
(if $requested == "" or $requested == $rm then 0
|
|
3778
4455
|
else
|
|
3779
|
-
($p.input[$rm] // null)
|
|
3780
|
-
($p.
|
|
3781
|
-
|
|
4456
|
+
($p.input[$rm] // null) as $rin | ($p.output[$rm] // null) as $rout |
|
|
4457
|
+
($p.cache_read[$rm] // null) as $rcr |
|
|
4458
|
+
($p.input[$requested] // null) as $sin | ($p.output[$requested] // null) as $sout |
|
|
4459
|
+
($p.cache_read[$requested] // null) as $scr |
|
|
4460
|
+
if ($rin == null or $rout == null or $rcr == null or $sin == null or $sout == null or $scr == null) then 0
|
|
3782
4461
|
else
|
|
3783
|
-
(($t.in + 1.25 * $t.cwrt +
|
|
3784
|
-
($t.
|
|
3785
|
-
($
|
|
3786
|
-
($
|
|
4462
|
+
(($t.in + 1.25 * $t.cwrt + $rcr * $t.crd) / 1000) as $routed_input_units |
|
|
4463
|
+
(($t.in + 1.25 * $t.cwrt + $scr * $t.crd) / 1000) as $requested_input_units |
|
|
4464
|
+
($t.out / 1000) as $output_units |
|
|
4465
|
+
($routed_input_units * $rin + $output_units * $rout) as $routed_cost |
|
|
4466
|
+
($requested_input_units * $sin + $output_units * $sout) as $requested_cost |
|
|
3787
4467
|
($requested_cost - $routed_cost)
|
|
3788
4468
|
end
|
|
3789
4469
|
end) as $savings |
|
|
@@ -4055,61 +4735,9 @@ fi
|
|
|
4055
4735
|
|
|
4056
4736
|
# ---------- post-install verification ----------
|
|
4057
4737
|
|
|
4058
|
-
|
|
4059
|
-
if ! spin "Pinging $base_url/health" curl -fsS --max-time 5 "$base_url/health"; then
|
|
4060
|
-
warn "Could not reach $base_url/health within 5s. Settings are written; verify the router is running."
|
|
4061
|
-
fi
|
|
4062
|
-
fi
|
|
4063
|
-
|
|
4064
|
-
if [ -n "$api_key" ]; then
|
|
4065
|
-
# Pass the router key via stdin (`@-`) instead of a -H argument so the key
|
|
4066
|
-
# never appears in the process arg list (visible via `ps` / /proc to other
|
|
4067
|
-
# local users on shared machines). We feed stdin via a small wrapper so the
|
|
4068
|
-
# spinner's exec form sees a single command argv.
|
|
4069
|
-
validate_key() {
|
|
4070
|
-
printf '%s: %s\n' "$router_key_header" "$api_key" \
|
|
4071
|
-
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
4072
|
-
}
|
|
4073
|
-
if ! spin "Validating API key" validate_key; then
|
|
4074
|
-
# A key we read back off disk can have been revoked or rotated since it was
|
|
4075
|
-
# installed, and reusing it silently would leave a broken install behind
|
|
4076
|
-
# exactly where the old behavior (always prompt) would have fixed it. Ask
|
|
4077
|
-
# once, then rewrite the settings with whatever the user pastes. Anywhere a
|
|
4078
|
-
# prompt isn't available — non-interactive, update, --quiet, no tty, or a
|
|
4079
|
-
# key that didn't come from disk — keep the historical warn-and-continue.
|
|
4080
|
-
if [ "$api_key_source" = "disk" ] && [ "$non_interactive" != "true" ] \
|
|
4081
|
-
&& [ "$quiet" != "true" ] && [ -r /dev/tty ]; then
|
|
4082
|
-
warn "The router key already installed was rejected (revoked or rotated)."
|
|
4083
|
-
prompt_for_key
|
|
4084
|
-
write_claude_settings "$api_key"
|
|
4085
|
-
if ! spin "Validating API key" validate_key; then
|
|
4086
|
-
warn "Router rejected the API key (check it matches the dashboard at $base_url)."
|
|
4087
|
-
fi
|
|
4088
|
-
elif [ "$mode" = "update" ]; then
|
|
4089
|
-
# update is meant for cron/scripting: a rejected key is a real failure,
|
|
4090
|
-
# not a note in the log. --quiet callers opted out of the noise, not out
|
|
4091
|
-
# of the nonzero exit — a scheduled run that reports success here would
|
|
4092
|
-
# hide a revoked key indefinitely.
|
|
4093
|
-
if [ "$quiet" = "true" ]; then
|
|
4094
|
-
warn "Router rejected the installed API key. Re-run the installer to set a new one."
|
|
4095
|
-
else
|
|
4096
|
-
err "Router rejected the installed API key. Re-run the installer to set a new one."
|
|
4097
|
-
fi
|
|
4098
|
-
exit 1
|
|
4099
|
-
else
|
|
4100
|
-
warn "Router rejected the API key (check it matches the dashboard at $base_url)."
|
|
4101
|
-
fi
|
|
4102
|
-
fi
|
|
4103
|
-
fi
|
|
4738
|
+
verify_install
|
|
4104
4739
|
|
|
4105
4740
|
# ---------- done ----------
|
|
4106
4741
|
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
printf "%s✓%s %s%sWeave Router config refreshed for Claude Code.%s\n" \
|
|
4110
|
-
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
4111
|
-
exit 0
|
|
4112
|
-
fi
|
|
4113
|
-
printf "%s✓%s %s%sWeave Router installed for Claude Code.%s\n" \
|
|
4114
|
-
"$C_GREEN" "$C_RESET" "$C_BOLD" "$C_BRAND" "$C_RESET"
|
|
4115
|
-
print_uninstall_hint
|
|
4742
|
+
announce_done "Claude Code"
|
|
4743
|
+
[ "$mode" = "update" ] || print_uninstall_hint
|