@workweave/router 0.1.1 → 0.1.3
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/cc-statusline.sh +10 -16
- package/install.sh +186 -20
- package/package.json +1 -1
package/cc-statusline.sh
CHANGED
|
@@ -130,10 +130,11 @@ normalize_model() {
|
|
|
130
130
|
prices='{
|
|
131
131
|
"input": {
|
|
132
132
|
"claude-haiku-4-5": 0.0008,
|
|
133
|
+
"claude-opus-4-6": 0.015,
|
|
133
134
|
"claude-opus-4-7": 0.015,
|
|
134
135
|
"claude-sonnet-4-5": 0.003,
|
|
135
136
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
136
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
137
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
137
138
|
"gemini-2.0-flash": 0.0001,
|
|
138
139
|
"gemini-2.0-flash-lite": 0.000075,
|
|
139
140
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -160,21 +161,18 @@ prices='{
|
|
|
160
161
|
"gpt-5.5-mini": 0.0005,
|
|
161
162
|
"gpt-5.5-nano": 0.00015,
|
|
162
163
|
"gpt-5.5-pro": 0.03,
|
|
163
|
-
"
|
|
164
|
-
"moonshotai/kimi-k2.5": 0.00044,
|
|
164
|
+
"moonshotai/kimi-k2.5": 0.0006,
|
|
165
165
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
166
|
-
"qwen/qwen3-
|
|
167
|
-
"qwen/qwen3-
|
|
168
|
-
"qwen/qwen3-coder-next": 0.00007,
|
|
169
|
-
"qwen/qwen3-next-80b-a3b-instruct": 0.00009,
|
|
170
|
-
"qwen/qwen3.5-flash-02-23": 0.000065
|
|
166
|
+
"qwen/qwen3-coder-next": 0.0005,
|
|
167
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.00015
|
|
171
168
|
},
|
|
172
169
|
"output": {
|
|
173
170
|
"claude-haiku-4-5": 0.004,
|
|
171
|
+
"claude-opus-4-6": 0.075,
|
|
174
172
|
"claude-opus-4-7": 0.075,
|
|
175
173
|
"claude-sonnet-4-5": 0.015,
|
|
176
174
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
177
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
175
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
178
176
|
"gemini-2.0-flash": 0.0004,
|
|
179
177
|
"gemini-2.0-flash-lite": 0.0003,
|
|
180
178
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -201,14 +199,10 @@ prices='{
|
|
|
201
199
|
"gpt-5.5-mini": 0.0025,
|
|
202
200
|
"gpt-5.5-nano": 0.0006,
|
|
203
201
|
"gpt-5.5-pro": 0.12,
|
|
204
|
-
"
|
|
205
|
-
"moonshotai/kimi-k2.5": 0.002,
|
|
202
|
+
"moonshotai/kimi-k2.5": 0.003,
|
|
206
203
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
207
|
-
"qwen/qwen3-
|
|
208
|
-
"qwen/qwen3-
|
|
209
|
-
"qwen/qwen3-coder-next": 0.0003,
|
|
210
|
-
"qwen/qwen3-next-80b-a3b-instruct": 0.0011,
|
|
211
|
-
"qwen/qwen3.5-flash-02-23": 0.00026
|
|
204
|
+
"qwen/qwen3-coder-next": 0.0012,
|
|
205
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.0012
|
|
212
206
|
}
|
|
213
207
|
}'
|
|
214
208
|
# END_GENERATED_PRICES
|
package/install.sh
CHANGED
|
@@ -207,6 +207,138 @@ refuse_if_symlink() {
|
|
|
207
207
|
fi
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
# ---------- identity helpers ----------
|
|
211
|
+
#
|
|
212
|
+
# The router parses X-Weave-User-Email and X-Weave-User-Name on every protocol
|
|
213
|
+
# (Anthropic, OpenAI, Gemini) and persists them onto router.model_router_users
|
|
214
|
+
# so customers can attribute traffic to a person even when many people share
|
|
215
|
+
# one API key. Claude Code's metadata.user_id carries only account_uuid (no
|
|
216
|
+
# email), so without these headers the router only ever sees anonymous UUIDs.
|
|
217
|
+
|
|
218
|
+
# normalize_email mirrors the router's proxy.NormalizeEmail: trim, lowercase,
|
|
219
|
+
# enforce a single '@' with non-empty local + domain parts, and cap at 254
|
|
220
|
+
# chars (RFC 5321). Returns the cleaned address on stdout, or empty string if
|
|
221
|
+
# the input is missing or malformed. We validate locally so the installer
|
|
222
|
+
# never plants a header value the router would silently drop, and so a
|
|
223
|
+
# typo'd git config doesn't end up as a per-request identity claim.
|
|
224
|
+
normalize_email() {
|
|
225
|
+
local raw="${1:-}"
|
|
226
|
+
# Trim whitespace then lowercase. tr is POSIX; the [:upper:]/[:lower:] form
|
|
227
|
+
# works on both macOS (BSD) and Linux (GNU) without needing LANG tweaks.
|
|
228
|
+
local trimmed="${raw#"${raw%%[![:space:]]*}"}"
|
|
229
|
+
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
230
|
+
local lowered
|
|
231
|
+
lowered="$(printf '%s' "$trimmed" | tr '[:upper:]' '[:lower:]')"
|
|
232
|
+
if [ -z "$lowered" ] || [ "${#lowered}" -gt 254 ]; then
|
|
233
|
+
printf ''
|
|
234
|
+
return
|
|
235
|
+
fi
|
|
236
|
+
# Reject any interior whitespace or control character so the value can't
|
|
237
|
+
# smuggle a second header into the newline-delimited ANTHROPIC_CUSTOM_HEADERS
|
|
238
|
+
# var. A valid email has none, so this is shape-only — not a deliverability
|
|
239
|
+
# check.
|
|
240
|
+
if printf '%s' "$lowered" | LC_ALL=C grep -q '[[:space:][:cntrl:]]'; then
|
|
241
|
+
printf ''
|
|
242
|
+
return
|
|
243
|
+
fi
|
|
244
|
+
case "$lowered" in
|
|
245
|
+
*@*@*) printf ''; return ;;
|
|
246
|
+
@*|*@) printf ''; return ;;
|
|
247
|
+
*@*) printf '%s' "$lowered" ;;
|
|
248
|
+
*) printf '' ;;
|
|
249
|
+
esac
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# normalize_name trims whitespace, rejects empty/oversized, and strips control
|
|
253
|
+
# chars + the colon/CR/LF chars HTTP forbids in header values. Display names
|
|
254
|
+
# are free-form so we don't case-fold; we just keep the header well-formed.
|
|
255
|
+
normalize_name() {
|
|
256
|
+
local raw="${1:-}"
|
|
257
|
+
local trimmed="${raw#"${raw%%[![:space:]]*}"}"
|
|
258
|
+
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
|
|
259
|
+
# Drop CR/LF/colon (header smuggling) and other control chars. tr's -d
|
|
260
|
+
# with a character class is portable across BSD/GNU.
|
|
261
|
+
local cleaned
|
|
262
|
+
cleaned="$(printf '%s' "$trimmed" | tr -d '\r\n:' | tr -d '[:cntrl:]')"
|
|
263
|
+
if [ -z "$cleaned" ] || [ "${#cleaned}" -gt 128 ]; then
|
|
264
|
+
printf ''
|
|
265
|
+
return
|
|
266
|
+
fi
|
|
267
|
+
printf '%s' "$cleaned"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# resolve_user_email picks the email to plant in router request headers so the
|
|
271
|
+
# router can attribute traffic to a person even on shared API keys. Priority:
|
|
272
|
+
# WEAVE_USER_EMAIL env override -> git config user.email -> interactive prompt
|
|
273
|
+
# (pre-filled with whatever we found). In --non-interactive mode we never
|
|
274
|
+
# prompt, so unset/invalid means we ship no header (router treats that as
|
|
275
|
+
# account_uuid-only, same as today). Echoes the validated email on stdout.
|
|
276
|
+
resolve_user_email() {
|
|
277
|
+
local candidate=""
|
|
278
|
+
if [ -n "${WEAVE_USER_EMAIL:-}" ]; then
|
|
279
|
+
candidate="$(normalize_email "$WEAVE_USER_EMAIL")"
|
|
280
|
+
if [ -z "$candidate" ]; then
|
|
281
|
+
warn "WEAVE_USER_EMAIL=\"$WEAVE_USER_EMAIL\" is not a valid email; ignoring."
|
|
282
|
+
fi
|
|
283
|
+
fi
|
|
284
|
+
if [ -z "$candidate" ]; then
|
|
285
|
+
local git_email
|
|
286
|
+
git_email="$(git config --global user.email 2>/dev/null || true)"
|
|
287
|
+
candidate="$(normalize_email "$git_email")"
|
|
288
|
+
fi
|
|
289
|
+
if [ "$non_interactive" = "true" ] || [ ! -r /dev/tty ]; then
|
|
290
|
+
printf '%s' "$candidate"
|
|
291
|
+
return
|
|
292
|
+
fi
|
|
293
|
+
if [ "$quiet" = "true" ]; then
|
|
294
|
+
printf '%s' "$candidate"
|
|
295
|
+
return
|
|
296
|
+
fi
|
|
297
|
+
local prompt_default="$candidate"
|
|
298
|
+
local response=""
|
|
299
|
+
if [ -n "$prompt_default" ]; then
|
|
300
|
+
printf "%sIdentify router traffic as %s[%s]%s (Enter to accept, '-' to skip): " \
|
|
301
|
+
"$C_DIM" "$C_BOLD" "$prompt_default" "$C_RESET" >/dev/tty
|
|
302
|
+
else
|
|
303
|
+
printf "%sEmail to identify your router traffic (blank to skip): %s" \
|
|
304
|
+
"$C_DIM" "$C_RESET" >/dev/tty
|
|
305
|
+
fi
|
|
306
|
+
read -r response </dev/tty || response=""
|
|
307
|
+
case "$response" in
|
|
308
|
+
"") printf '%s' "$prompt_default" ;;
|
|
309
|
+
"-") printf '' ;;
|
|
310
|
+
*)
|
|
311
|
+
local cleaned
|
|
312
|
+
cleaned="$(normalize_email "$response")"
|
|
313
|
+
if [ -z "$cleaned" ]; then
|
|
314
|
+
warn "\"$response\" is not a valid email; skipping identity header."
|
|
315
|
+
fi
|
|
316
|
+
printf '%s' "$cleaned"
|
|
317
|
+
;;
|
|
318
|
+
esac
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
# resolve_user_name mirrors resolve_user_email but for display name. Priority:
|
|
322
|
+
# WEAVE_USER_NAME env override -> git config user.name -> empty. We don't
|
|
323
|
+
# prompt for name independently: if email prompting yielded nothing, name
|
|
324
|
+
# almost certainly will too, and a second prompt is noise. Echoes the
|
|
325
|
+
# validated name on stdout.
|
|
326
|
+
resolve_user_name() {
|
|
327
|
+
local candidate=""
|
|
328
|
+
if [ -n "${WEAVE_USER_NAME:-}" ]; then
|
|
329
|
+
candidate="$(normalize_name "$WEAVE_USER_NAME")"
|
|
330
|
+
if [ -z "$candidate" ]; then
|
|
331
|
+
warn "WEAVE_USER_NAME=\"$WEAVE_USER_NAME\" is not a usable name; ignoring."
|
|
332
|
+
fi
|
|
333
|
+
fi
|
|
334
|
+
if [ -z "$candidate" ]; then
|
|
335
|
+
local git_name
|
|
336
|
+
git_name="$(git config --global user.name 2>/dev/null || true)"
|
|
337
|
+
candidate="$(normalize_name "$git_name")"
|
|
338
|
+
fi
|
|
339
|
+
printf '%s' "$candidate"
|
|
340
|
+
}
|
|
341
|
+
|
|
210
342
|
# ---------- uninstall delegation ----------
|
|
211
343
|
#
|
|
212
344
|
# `--uninstall` flips this script into a thin shim for uninstall.sh: the
|
|
@@ -444,7 +576,8 @@ if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
|
444
576
|
# _spin_cleanup (installed globally above) already restores stty echo on
|
|
445
577
|
# any exit path, so we don't need a separate trap here — that would
|
|
446
578
|
# overwrite the spinner cleanup and leak the cursor / child PID on Ctrl-C.
|
|
447
|
-
printf "%
|
|
579
|
+
printf "%sGet your Weave Router API key at %s%s\n" "$C_BRAND" "$base_url" "$C_RESET"
|
|
580
|
+
printf "%sPaste your key here (rk_...):%s " "$C_DIM" "$C_RESET"
|
|
448
581
|
stty -echo </dev/tty 2>/dev/null || true
|
|
449
582
|
read -r api_key </dev/tty
|
|
450
583
|
stty echo </dev/tty 2>/dev/null || true
|
|
@@ -452,6 +585,32 @@ if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
|
452
585
|
[ -n "$api_key" ] || { err "no key provided"; exit 1; }
|
|
453
586
|
fi
|
|
454
587
|
|
|
588
|
+
# ---------- identity (user email + name) ----------
|
|
589
|
+
#
|
|
590
|
+
# Resolved after api_key so the prompt sequence reads naturally: key first,
|
|
591
|
+
# then "who are you?". Both end up as request headers the router parses on
|
|
592
|
+
# every protocol so customers can link router users to Weave accounts on the
|
|
593
|
+
# dashboard. Empty values = no header (router stays in account_uuid-only
|
|
594
|
+
# mode), so existing behavior is preserved when git config is unset.
|
|
595
|
+
user_email="$(resolve_user_email)"
|
|
596
|
+
# Gate name on email: when the user explicitly opts out of email identity
|
|
597
|
+
# (via '-' at the prompt or by clearing git config), don't auto-plant a
|
|
598
|
+
# name from git config either. Opt-out should be all-or-nothing so the
|
|
599
|
+
# router consistently sees zero identity headers when the user wants to
|
|
600
|
+
# stay anonymous.
|
|
601
|
+
if [ -n "$user_email" ]; then
|
|
602
|
+
user_name="$(resolve_user_name)"
|
|
603
|
+
else
|
|
604
|
+
user_name=""
|
|
605
|
+
fi
|
|
606
|
+
if [ -n "$user_email" ] && [ -n "$user_name" ]; then
|
|
607
|
+
ok "Will identify router traffic as $user_name <$user_email>"
|
|
608
|
+
elif [ -n "$user_email" ]; then
|
|
609
|
+
ok "Will identify router traffic as $user_email"
|
|
610
|
+
else
|
|
611
|
+
info "No identity set — router traffic will be attributed by account UUID only."
|
|
612
|
+
fi
|
|
613
|
+
|
|
455
614
|
# ---------- write the statusline script ----------
|
|
456
615
|
|
|
457
616
|
cat > "$statusline_file" << 'STATUSLINE_EOF'
|
|
@@ -587,10 +746,11 @@ normalize_model() {
|
|
|
587
746
|
prices='{
|
|
588
747
|
"input": {
|
|
589
748
|
"claude-haiku-4-5": 0.0008,
|
|
749
|
+
"claude-opus-4-6": 0.015,
|
|
590
750
|
"claude-opus-4-7": 0.015,
|
|
591
751
|
"claude-sonnet-4-5": 0.003,
|
|
592
752
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
593
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
753
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
594
754
|
"gemini-2.0-flash": 0.0001,
|
|
595
755
|
"gemini-2.0-flash-lite": 0.000075,
|
|
596
756
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -617,21 +777,18 @@ prices='{
|
|
|
617
777
|
"gpt-5.5-mini": 0.0005,
|
|
618
778
|
"gpt-5.5-nano": 0.00015,
|
|
619
779
|
"gpt-5.5-pro": 0.03,
|
|
620
|
-
"
|
|
621
|
-
"moonshotai/kimi-k2.5": 0.00044,
|
|
780
|
+
"moonshotai/kimi-k2.5": 0.0006,
|
|
622
781
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
623
|
-
"qwen/qwen3-
|
|
624
|
-
"qwen/qwen3-
|
|
625
|
-
"qwen/qwen3-coder-next": 0.00007,
|
|
626
|
-
"qwen/qwen3-next-80b-a3b-instruct": 0.00009,
|
|
627
|
-
"qwen/qwen3.5-flash-02-23": 0.000065
|
|
782
|
+
"qwen/qwen3-coder-next": 0.0005,
|
|
783
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.00015
|
|
628
784
|
},
|
|
629
785
|
"output": {
|
|
630
786
|
"claude-haiku-4-5": 0.004,
|
|
787
|
+
"claude-opus-4-6": 0.075,
|
|
631
788
|
"claude-opus-4-7": 0.075,
|
|
632
789
|
"claude-sonnet-4-5": 0.015,
|
|
633
790
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
634
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
791
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
635
792
|
"gemini-2.0-flash": 0.0004,
|
|
636
793
|
"gemini-2.0-flash-lite": 0.0003,
|
|
637
794
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -658,14 +815,10 @@ prices='{
|
|
|
658
815
|
"gpt-5.5-mini": 0.0025,
|
|
659
816
|
"gpt-5.5-nano": 0.0006,
|
|
660
817
|
"gpt-5.5-pro": 0.12,
|
|
661
|
-
"
|
|
662
|
-
"moonshotai/kimi-k2.5": 0.002,
|
|
818
|
+
"moonshotai/kimi-k2.5": 0.003,
|
|
663
819
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
664
|
-
"qwen/qwen3-
|
|
665
|
-
"qwen/qwen3-
|
|
666
|
-
"qwen/qwen3-coder-next": 0.0003,
|
|
667
|
-
"qwen/qwen3-next-80b-a3b-instruct": 0.0011,
|
|
668
|
-
"qwen/qwen3.5-flash-02-23": 0.00026
|
|
820
|
+
"qwen/qwen3-coder-next": 0.0012,
|
|
821
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.0012
|
|
669
822
|
}
|
|
670
823
|
}'
|
|
671
824
|
# END_GENERATED_PRICES
|
|
@@ -836,13 +989,26 @@ tmp_patch="$(mktemp)"
|
|
|
836
989
|
# leave the cursor hidden if Ctrl-C lands during settings.json patching.
|
|
837
990
|
trap '_spin_cleanup; rm -f "$tmp_patch"' EXIT INT TERM HUP
|
|
838
991
|
|
|
992
|
+
# Claude Code splits ANTHROPIC_CUSTOM_HEADERS on newlines, so multiple headers
|
|
993
|
+
# ride in the same env var separated by \n. Append identity headers alongside
|
|
994
|
+
# the router key so a single var carries them all. When email/name are empty
|
|
995
|
+
# we keep the bare router-key form so a re-install for a user who opted out
|
|
996
|
+
# cleanly removes the old line.
|
|
997
|
+
custom_headers="$router_key_header: $api_key"
|
|
998
|
+
if [ -n "$user_email" ]; then
|
|
999
|
+
custom_headers="$custom_headers"$'\n'"X-Weave-User-Email: $user_email"
|
|
1000
|
+
fi
|
|
1001
|
+
if [ -n "$user_name" ]; then
|
|
1002
|
+
custom_headers="$custom_headers"$'\n'"X-Weave-User-Name: $user_name"
|
|
1003
|
+
fi
|
|
1004
|
+
|
|
839
1005
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
840
1006
|
jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
|
|
841
1007
|
env: { ANTHROPIC_BASE_URL: $url },
|
|
842
1008
|
statusLine: { type: "command", command: $sl }
|
|
843
1009
|
}' >"$tmp_patch"
|
|
844
1010
|
else
|
|
845
|
-
jq -n --arg url "$base_url" --arg header "$
|
|
1011
|
+
jq -n --arg url "$base_url" --arg header "$custom_headers" --arg sl "$statusline_path_for_settings" '{
|
|
846
1012
|
env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header },
|
|
847
1013
|
statusLine: { type: "command", command: $sl }
|
|
848
1014
|
}' >"$tmp_patch"
|
|
@@ -868,7 +1034,7 @@ fi
|
|
|
868
1034
|
ok "Settings written to $settings_file"
|
|
869
1035
|
|
|
870
1036
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
871
|
-
jq -n --arg header "$
|
|
1037
|
+
jq -n --arg header "$custom_headers" '{
|
|
872
1038
|
env: { ANTHROPIC_CUSTOM_HEADERS: $header }
|
|
873
1039
|
}' >"$tmp_patch"
|
|
874
1040
|
if [ -f "$local_settings_file" ]; then
|
|
@@ -925,7 +1091,7 @@ if [ -n "$api_key" ]; then
|
|
|
925
1091
|
| curl -fsS --max-time 5 --header @- "$base_url/validate"
|
|
926
1092
|
}
|
|
927
1093
|
if ! spin "Validating API key" validate_key; then
|
|
928
|
-
warn "Router rejected the API key (check it matches the dashboard at $base_url
|
|
1094
|
+
warn "Router rejected the API key (check it matches the dashboard at $base_url)."
|
|
929
1095
|
fi
|
|
930
1096
|
fi
|
|
931
1097
|
|