@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.
Files changed (3) hide show
  1. package/cc-statusline.sh +10 -16
  2. package/install.sh +186 -20
  3. 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.000435,
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
- "mistralai/mistral-small-2603": 0.00015,
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,
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.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,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
- "mistralai/mistral-small-2603": 0.0006,
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-30b-a3b-instruct-2507": 0.00033,
208
- "qwen/qwen3-coder": 0.0018,
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 "%sPaste your Weave Router API key (rk_...):%s " "$C_DIM" "$C_RESET"
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.000435,
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
- "mistralai/mistral-small-2603": 0.00015,
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-30b-a3b-instruct-2507": 0.00008,
624
- "qwen/qwen3-coder": 0.00022,
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.00087,
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
- "mistralai/mistral-small-2603": 0.0006,
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,
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 "$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" '{
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 "$router_key_header: $api_key" '{
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/ui/)."
1094
+ warn "Router rejected the API key (check it matches the dashboard at $base_url)."
929
1095
  fi
930
1096
  fi
931
1097
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workweave/router",
3
- "version": "0.1.1",
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"