@workweave/router 0.1.2 → 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 +8 -12
- package/install.sh +181 -14
- package/package.json +1 -1
package/cc-statusline.sh
CHANGED
|
@@ -134,7 +134,7 @@ prices='{
|
|
|
134
134
|
"claude-opus-4-7": 0.015,
|
|
135
135
|
"claude-sonnet-4-5": 0.003,
|
|
136
136
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
137
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
137
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
138
138
|
"gemini-2.0-flash": 0.0001,
|
|
139
139
|
"gemini-2.0-flash-lite": 0.000075,
|
|
140
140
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -161,12 +161,10 @@ prices='{
|
|
|
161
161
|
"gpt-5.5-mini": 0.0005,
|
|
162
162
|
"gpt-5.5-nano": 0.00015,
|
|
163
163
|
"gpt-5.5-pro": 0.03,
|
|
164
|
-
"moonshotai/kimi-k2.5": 0.
|
|
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
|
|
166
|
+
"qwen/qwen3-coder-next": 0.0005,
|
|
167
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.00015
|
|
170
168
|
},
|
|
171
169
|
"output": {
|
|
172
170
|
"claude-haiku-4-5": 0.004,
|
|
@@ -174,7 +172,7 @@ prices='{
|
|
|
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,12 +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
|
-
"moonshotai/kimi-k2.5": 0.
|
|
202
|
+
"moonshotai/kimi-k2.5": 0.003,
|
|
205
203
|
"qwen/qwen3-235b-a22b-2507": 0.000463,
|
|
206
|
-
"qwen/qwen3-
|
|
207
|
-
"qwen/qwen3-
|
|
208
|
-
"qwen/qwen3-coder-next": 0.0003,
|
|
209
|
-
"qwen/qwen3-next-80b-a3b-instruct": 0.0011
|
|
204
|
+
"qwen/qwen3-coder-next": 0.0012,
|
|
205
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.0012
|
|
210
206
|
}
|
|
211
207
|
}'
|
|
212
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
|
|
@@ -453,6 +585,32 @@ if [ -n "${WEAVE_ROUTER_KEY:-}" ]; then
|
|
|
453
585
|
[ -n "$api_key" ] || { err "no key provided"; exit 1; }
|
|
454
586
|
fi
|
|
455
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
|
+
|
|
456
614
|
# ---------- write the statusline script ----------
|
|
457
615
|
|
|
458
616
|
cat > "$statusline_file" << 'STATUSLINE_EOF'
|
|
@@ -592,7 +750,7 @@ prices='{
|
|
|
592
750
|
"claude-opus-4-7": 0.015,
|
|
593
751
|
"claude-sonnet-4-5": 0.003,
|
|
594
752
|
"deepseek/deepseek-v4-flash": 0.00014,
|
|
595
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
753
|
+
"deepseek/deepseek-v4-pro": 0.00174,
|
|
596
754
|
"gemini-2.0-flash": 0.0001,
|
|
597
755
|
"gemini-2.0-flash-lite": 0.000075,
|
|
598
756
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -619,12 +777,10 @@ prices='{
|
|
|
619
777
|
"gpt-5.5-mini": 0.0005,
|
|
620
778
|
"gpt-5.5-nano": 0.00015,
|
|
621
779
|
"gpt-5.5-pro": 0.03,
|
|
622
|
-
"moonshotai/kimi-k2.5": 0.
|
|
780
|
+
"moonshotai/kimi-k2.5": 0.0006,
|
|
623
781
|
"qwen/qwen3-235b-a22b-2507": 0.000071,
|
|
624
|
-
"qwen/qwen3-
|
|
625
|
-
"qwen/qwen3-
|
|
626
|
-
"qwen/qwen3-coder-next": 0.00007,
|
|
627
|
-
"qwen/qwen3-next-80b-a3b-instruct": 0.00009
|
|
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,
|
|
@@ -632,7 +788,7 @@ prices='{
|
|
|
632
788
|
"claude-opus-4-7": 0.075,
|
|
633
789
|
"claude-sonnet-4-5": 0.015,
|
|
634
790
|
"deepseek/deepseek-v4-flash": 0.00028,
|
|
635
|
-
"deepseek/deepseek-v4-pro": 0.
|
|
791
|
+
"deepseek/deepseek-v4-pro": 0.00348,
|
|
636
792
|
"gemini-2.0-flash": 0.0004,
|
|
637
793
|
"gemini-2.0-flash-lite": 0.0003,
|
|
638
794
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -659,12 +815,10 @@ prices='{
|
|
|
659
815
|
"gpt-5.5-mini": 0.0025,
|
|
660
816
|
"gpt-5.5-nano": 0.0006,
|
|
661
817
|
"gpt-5.5-pro": 0.12,
|
|
662
|
-
"moonshotai/kimi-k2.5": 0.
|
|
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
|
|
820
|
+
"qwen/qwen3-coder-next": 0.0012,
|
|
821
|
+
"qwen/qwen3-next-80b-a3b-instruct": 0.0012
|
|
668
822
|
}
|
|
669
823
|
}'
|
|
670
824
|
# END_GENERATED_PRICES
|
|
@@ -835,13 +989,26 @@ tmp_patch="$(mktemp)"
|
|
|
835
989
|
# leave the cursor hidden if Ctrl-C lands during settings.json patching.
|
|
836
990
|
trap '_spin_cleanup; rm -f "$tmp_patch"' EXIT INT TERM HUP
|
|
837
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
|
+
|
|
838
1005
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
839
1006
|
jq -n --arg url "$base_url" --arg sl "$statusline_path_for_settings" '{
|
|
840
1007
|
env: { ANTHROPIC_BASE_URL: $url },
|
|
841
1008
|
statusLine: { type: "command", command: $sl }
|
|
842
1009
|
}' >"$tmp_patch"
|
|
843
1010
|
else
|
|
844
|
-
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" '{
|
|
845
1012
|
env: { ANTHROPIC_BASE_URL: $url, ANTHROPIC_CUSTOM_HEADERS: $header },
|
|
846
1013
|
statusLine: { type: "command", command: $sl }
|
|
847
1014
|
}' >"$tmp_patch"
|
|
@@ -867,7 +1034,7 @@ fi
|
|
|
867
1034
|
ok "Settings written to $settings_file"
|
|
868
1035
|
|
|
869
1036
|
if [ "$scope" = "project" ] && [ -z "$install_dir" ]; then
|
|
870
|
-
jq -n --arg header "$
|
|
1037
|
+
jq -n --arg header "$custom_headers" '{
|
|
871
1038
|
env: { ANTHROPIC_CUSTOM_HEADERS: $header }
|
|
872
1039
|
}' >"$tmp_patch"
|
|
873
1040
|
if [ -f "$local_settings_file" ]; then
|