@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 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.000435,
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.00044,
164
+ "moonshotai/kimi-k2.5": 0.0006,
165
165
  "qwen/qwen3-235b-a22b-2507": 0.000071,
166
- "qwen/qwen3-30b-a3b-instruct-2507": 0.00008,
167
- "qwen/qwen3-coder": 0.00022,
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.00087,
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.002,
202
+ "moonshotai/kimi-k2.5": 0.003,
205
203
  "qwen/qwen3-235b-a22b-2507": 0.000463,
206
- "qwen/qwen3-30b-a3b-instruct-2507": 0.00033,
207
- "qwen/qwen3-coder": 0.0018,
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.000435,
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.00044,
780
+ "moonshotai/kimi-k2.5": 0.0006,
623
781
  "qwen/qwen3-235b-a22b-2507": 0.000071,
624
- "qwen/qwen3-30b-a3b-instruct-2507": 0.00008,
625
- "qwen/qwen3-coder": 0.00022,
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.00087,
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.002,
818
+ "moonshotai/kimi-k2.5": 0.003,
663
819
  "qwen/qwen3-235b-a22b-2507": 0.000463,
664
- "qwen/qwen3-30b-a3b-instruct-2507": 0.00033,
665
- "qwen/qwen3-coder": 0.0018,
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 "$router_key_header: $api_key" --arg sl "$statusline_path_for_settings" '{
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 "$router_key_header: $api_key" '{
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "One-command installer that points Claude Code at the Weave Router.",
5
5
  "bin": {
6
6
  "weave-router": "bin.js"