@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.
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env bash
2
+ # <!-- weave-router managed codex status -->
3
+ #
4
+ # Codex lifecycle hook for the Weave Router. Codex passes a JSON object on
5
+ # stdin; the Stop hook includes the last assistant message, which carries the
6
+ # router's routed-model marker when the selected model changes. The helper
7
+ # keeps the last known routed model per session and reflects it in the terminal
8
+ # title, so the active router remains visible between turns without injecting
9
+ # another message into the conversation.
10
+ #
11
+ # Savings come from the router, not from local arithmetic. Codex records its
12
+ # own requested model on every turn and never the served one, so the per-turn
13
+ # pricing the Claude Code statusline does cannot be reproduced here — it would
14
+ # price both sides of the comparison at the same model and report zero. The
15
+ # router already sums the real thing per session, so the hook fetches
16
+ # GET <base>/v1/sessions/<id>/cost and renders what it returns. The fetch runs
17
+ # in a detached subshell writing a cache the NEXT turn reads, so no turn ever
18
+ # blocks on the network, and every failure path leaves the title model-only.
19
+
20
+ set -euo pipefail
21
+
22
+ state_root="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router/codex"
23
+ helper_dir="$(cd "$(dirname "$0")" 2>/dev/null && pwd -P)"
24
+ disabled_marker="$helper_dir/.weave-router-disabled"
25
+ router_badge_sentinel=$'⁣⁠⁣⁠'
26
+ # Must stay verbatim in sync with install.sh / uninstall.sh: the endpoint read
27
+ # below is scoped to this block so a key-shaped string elsewhere in the user's
28
+ # config.toml is never adopted.
29
+ codex_begin_marker="# >>> weave-router managed (do not edit between markers) >>>"
30
+ codex_end_marker="# <<< weave-router managed <<<"
31
+
32
+ emit_title() {
33
+ local title="$1"
34
+ if [ -n "${WEAVE_CODEX_STATUS_TITLE_FILE:-}" ]; then
35
+ printf '%s\n' "$title" >"$WEAVE_CODEX_STATUS_TITLE_FILE"
36
+ elif [ -w /dev/tty ]; then
37
+ printf '\033]0;%s\007' "$title" >/dev/tty
38
+ fi
39
+ }
40
+
41
+ safe_session_id() {
42
+ local id="$1"
43
+ case "$id" in
44
+ ''|*[!A-Za-z0-9._-]*) return 1 ;;
45
+ esac
46
+ [ "${#id}" -le 128 ] || return 1
47
+ printf '%s' "$id"
48
+ }
49
+
50
+ safe_display_value() {
51
+ printf '%s' "$1" | sed 's/[^A-Za-z0-9._:\/-]//g' | cut -c1-128
52
+ }
53
+
54
+ state_file_for() {
55
+ local id
56
+ id="$(safe_session_id "$1")" || return 1
57
+ printf '%s/%s.state' "$state_root" "$id"
58
+ }
59
+
60
+ read_state() {
61
+ local file="$1" key value
62
+ [ -f "$file" ] || return 0
63
+ while IFS='=' read -r key value; do
64
+ case "$key" in
65
+ routed_model) routed_model="$value" ;;
66
+ esac
67
+ done <"$file"
68
+ }
69
+
70
+ write_state() {
71
+ local file="$1" tmp
72
+ mkdir -p "$state_root"
73
+ chmod 700 "$state_root"
74
+ tmp="$(mktemp "$state_root/.state.XXXXXX")"
75
+ printf 'requested_model=%s\nrouted_model=%s\n' "$requested_model" "$routed_model" >"$tmp"
76
+ chmod 600 "$tmp"
77
+ mv "$tmp" "$file"
78
+ }
79
+
80
+ cost_file_for() {
81
+ local id
82
+ id="$(safe_session_id "$1")" || return 1
83
+ printf '%s/%s.cost' "$state_root" "$id"
84
+ }
85
+
86
+ # Reads the router base URL and key out of the Codex config this install owns.
87
+ # Resolved from the helper's own location first so a project-scope install never
88
+ # reads (or leaks) the user-scope key: the project helper lives in the same
89
+ # .codex directory as its config, while the user-scope helper sits in ~/.weave
90
+ # and reads ~/.codex. Values are scoped to the managed block so a key-shaped
91
+ # string the user wrote elsewhere in the file is never adopted. awk, not a TOML
92
+ # parser, because the Codex target deliberately does not require jq for config
93
+ # reads.
94
+ read_codex_endpoint() {
95
+ local config=""
96
+ # Project/custom installs name the helper weave-status.sh and keep it next
97
+ # to their config.toml. Falling through to ~/.codex would fetch another
98
+ # installation's session cost with that key. The user-scope helper
99
+ # (codex-status.sh in ~/.weave) has no adjacent config and owns HOME.
100
+ if [ -f "$helper_dir/config.toml" ]; then
101
+ config="$helper_dir/config.toml"
102
+ elif [ "$(basename "$0")" != "weave-status.sh" ] && [ -f "$HOME/.codex/config.toml" ]; then
103
+ config="$HOME/.codex/config.toml"
104
+ fi
105
+ [ -n "$config" ] || return 0
106
+ awk -v begin="$codex_begin_marker" -v end="$codex_end_marker" '
107
+ $0 == begin { inblk = 1; next }
108
+ $0 == end { inblk = 0; next }
109
+ !inblk { next }
110
+ match($0, /base_url[[:space:]]*=[[:space:]]*"[^"]*"/) {
111
+ v = substr($0, RSTART, RLENGTH)
112
+ sub(/^.*=[[:space:]]*"/, "", v); sub(/"$/, "", v)
113
+ url = v
114
+ }
115
+ match($0, /"X-Weave-Router-Key"[[:space:]]*=[[:space:]]*"[^"]*"/) {
116
+ v = substr($0, RSTART, RLENGTH)
117
+ sub(/^.*=[[:space:]]*"/, "", v); sub(/"$/, "", v)
118
+ key = v
119
+ }
120
+ END { if (url != "" && key != "") printf "%s\n%s\n", url, key }
121
+ ' "$config" 2>/dev/null || true
122
+ }
123
+
124
+ # Kicks off a detached fetch of this session's committed cost. The result lands
125
+ # in a cache the next turn reads; this turn renders whatever is already there.
126
+ # Fire-and-forget on purpose — a slow or unreachable router must never stall a
127
+ # Codex turn, and every failure simply leaves the previous cache in place.
128
+ refresh_session_cost() {
129
+ local id="$1" file="$2"
130
+ [ "${WEAVE_CODEX_STATUS_SAVINGS:-1}" = "0" ] && return 0
131
+ command -v curl >/dev/null 2>&1 || return 0
132
+
133
+ local endpoint base_url key
134
+ endpoint="$(read_codex_endpoint)" || return 0
135
+ base_url="$(printf '%s' "$endpoint" | sed -n 1p)"
136
+ key="$(printf '%s' "$endpoint" | sed -n 2p)"
137
+ [ -n "$base_url" ] && [ -n "$key" ] || return 0
138
+
139
+ mkdir -p "$state_root" 2>/dev/null || return 0
140
+ chmod 700 "$state_root" 2>/dev/null || true
141
+
142
+ (
143
+ exec </dev/null
144
+ # mkdir is the portable atomic test-and-set. A crashed holder would block
145
+ # refreshes forever, so a lock older than the fetch timeout is reclaimed.
146
+ lock="$file.lock"
147
+ if ! mkdir "$lock" 2>/dev/null; then
148
+ lock_mtime="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)" || lock_mtime=0
149
+ lock_now="$(date +%s 2>/dev/null)" || lock_now=0
150
+ if [ "${lock_mtime:-0}" -le 0 ] || [ $(( lock_now - lock_mtime )) -le 30 ]; then
151
+ exit 0
152
+ fi
153
+ rm -rf "$lock" 2>/dev/null
154
+ mkdir "$lock" 2>/dev/null || exit 0
155
+ fi
156
+ trap 'rmdir "$lock" 2>/dev/null' EXIT
157
+
158
+ # A file:// base is the offline/test seam: curl reads it as the response
159
+ # body directly, so the endpoint path is meaningless for it.
160
+ url="${base_url%/}"
161
+ case "$url" in
162
+ file://*) ;;
163
+ *) url="${url%/v1}/v1/sessions/$id/cost" ;;
164
+ esac
165
+ body="$(curl -fsS --max-time 5 -H "X-Weave-Router-Key: $key" "$url" 2>/dev/null)" || exit 0
166
+ # savings_usd is the router's own (requested - actual). A body without it
167
+ # (404, error envelope, older router) writes nothing and leaves the cache.
168
+ savings="$(printf '%s' "$body" | jq -r '.savings_usd // empty' 2>/dev/null)" || exit 0
169
+ case "$savings" in
170
+ ''|*[!0-9.eE+-]*) exit 0 ;;
171
+ esac
172
+ tmp="$file.tmp.$$"
173
+ mkdir -p "$(dirname "$file")" 2>/dev/null
174
+ if printf '%s' "$savings" >"$tmp" 2>/dev/null; then
175
+ chmod 600 "$tmp" 2>/dev/null
176
+ mv "$tmp" "$file" 2>/dev/null
177
+ fi
178
+ rm -f "$tmp" 2>/dev/null
179
+ ) >/dev/null 2>&1 &
180
+ disown 2>/dev/null || true
181
+ }
182
+
183
+ # Renders the cached savings as a display clause, or nothing. Values below a
184
+ # cent read as "<$0.01" rather than "$0.00", which would be indistinguishable
185
+ # from "the router ran and did not beat your selection". Negative totals are
186
+ # omitted entirely: the router picked a pricier model for quality on this
187
+ # session and "saved -$0.02" is a worse answer than staying quiet.
188
+ savings_clause() {
189
+ local file="$1" raw
190
+ [ "${WEAVE_CODEX_STATUS_SAVINGS:-1}" = "0" ] && return 0
191
+ [ -f "$file" ] || return 0
192
+ raw="$(cat "$file" 2>/dev/null)" || return 0
193
+ case "$raw" in
194
+ ''|*[!0-9.eE+-]*) return 0 ;;
195
+ esac
196
+ awk -v v="$raw" 'BEGIN{
197
+ v = v + 0
198
+ if (v < 0.005) { exit }
199
+ if (v < 0.01) { printf " · saved <$0.01"; exit }
200
+ printf " · saved $%.2f", v
201
+ }' 2>/dev/null || true
202
+ }
203
+
204
+ set -e
205
+
206
+ case "${1:-hook}" in
207
+ --direct)
208
+ emit_title "Codex · direct"
209
+ exit 0
210
+ ;;
211
+ --on)
212
+ rm -f "$disabled_marker"
213
+ emit_title "Weave Router · active"
214
+ exit 0
215
+ ;;
216
+ --off)
217
+ [ ! -L "$disabled_marker" ] || exit 0
218
+ : >"$disabled_marker"
219
+ chmod 600 "$disabled_marker"
220
+ emit_title "Codex · direct"
221
+ exit 0
222
+ ;;
223
+ esac
224
+
225
+ payload="$(cat)"
226
+ [ -n "$payload" ] || exit 0
227
+ command -v jq >/dev/null 2>&1 || exit 0
228
+ jq -e . >/dev/null 2>&1 <<<"$payload" || exit 0
229
+
230
+ hook_event_name="$(jq -r '.hook_event_name // ""' <<<"$payload")"
231
+ if [ "$hook_event_name" = "SessionStart" ]; then
232
+ if [ -f "$disabled_marker" ]; then
233
+ emit_title "Codex · direct"
234
+ jq -cn '{systemMessage:"Codex direct · Weave Router is off"}'
235
+ else
236
+ emit_title "Weave Router · active"
237
+ jq -cn '{systemMessage:"Weave Router active · routed model appears in the terminal title"}'
238
+ fi
239
+ exit 0
240
+ fi
241
+
242
+ if [ -f "$disabled_marker" ]; then
243
+ exit 0
244
+ fi
245
+
246
+ requested_model="$(safe_display_value "$(jq -r '.model // ""' <<<"$payload")")"
247
+ routed_model=""
248
+ session_id="$(jq -r '.session_id // ""' <<<"$payload")"
249
+ last_assistant_message="$(jq -r '.last_assistant_message // ""' <<<"$payload")"
250
+
251
+ file=""
252
+ if file="$(state_file_for "$session_id" 2>/dev/null)"; then
253
+ read_state "$file"
254
+ fi
255
+
256
+ # The marker is intentionally matched only at the beginning of the assistant
257
+ # message. Do not treat ordinary prose that mentions the heading as metadata.
258
+ first_line="${last_assistant_message%%$'\n'*}"
259
+ marker_model=""
260
+ force_model=""
261
+ case "$first_line" in
262
+ "${router_badge_sentinel}✦ **Weave Router** → "*)
263
+ marker_model="${first_line#"${router_badge_sentinel}✦ **Weave Router** → "}"
264
+ marker_model="${marker_model%% ·*}"
265
+ ;;
266
+ "✦ **Weave Router** → "*)
267
+ marker_model="${first_line#"✦ **Weave Router** → "}"
268
+ marker_model="${marker_model%% ·*}"
269
+ ;;
270
+ esac
271
+ case "$first_line" in
272
+ "Weave Router: force-model applied: "*" ("*)
273
+ force_model="${first_line#"Weave Router: force-model applied: "}"
274
+ force_model="${force_model%% (*}"
275
+ ;;
276
+ esac
277
+ if [ -n "$marker_model" ]; then
278
+ routed_model="$(safe_display_value "$marker_model")"
279
+ elif [ -n "$force_model" ]; then
280
+ routed_model="$(safe_display_value "$force_model")"
281
+ else
282
+ routed_model="$(safe_display_value "$routed_model")"
283
+ fi
284
+
285
+ if [ -n "$file" ]; then
286
+ write_state "$file"
287
+ fi
288
+
289
+ # The cache holds the previous turn's fetch; the refresh below serves the next
290
+ # one. Router telemetry is written asynchronously anyway, so a just-finished
291
+ # turn would not be included even in a blocking read — reading first and
292
+ # refreshing after costs a turn of freshness and buys never blocking Codex.
293
+ savings=""
294
+ cost_file=""
295
+ if cost_file="$(cost_file_for "$session_id" 2>/dev/null)"; then
296
+ savings="$(savings_clause "$cost_file")"
297
+ refresh_session_cost "$(safe_session_id "$session_id")" "$cost_file"
298
+ fi
299
+
300
+ if [ -n "$routed_model" ] && [ -n "$requested_model" ] && [ "$routed_model" != "$requested_model" ]; then
301
+ title="Weave Router · $routed_model ← $requested_model$savings"
302
+ elif [ -n "$routed_model" ]; then
303
+ title="Weave Router · $routed_model$savings"
304
+ elif [ -n "$requested_model" ]; then
305
+ title="Weave Router · active ← $requested_model$savings"
306
+ else
307
+ title="Weave Router · active$savings"
308
+ fi
309
+ emit_title "$title"
310
+ if [ -n "$marker_model" ] || [ -n "$force_model" ]; then
311
+ printf '%s' "$title" | jq -Rc '{systemMessage: .}'
312
+ fi
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Toggle beta HMM routing for this session.
3
+ ---
4
+
5
+ /beta $ARGUMENTS
package/directives.tsv CHANGED
@@ -4,9 +4,10 @@
4
4
  force-model|fm|prompt|yes|yes|yes|yes|manual|command,skill
5
5
  unforce-model|ufm|prompt|yes|yes|yes|yes|manual|command,skill
6
6
  router-feedback|rf|prompt|yes|yes|yes|no|manual|command,skill
7
- router-off||local-toggle|yes|no|no|no|manual|command
8
- router-on||local-toggle|yes|no|no|no|manual|command
9
- router-status||local-toggle|yes|no|no|no|manual|command
7
+ router-off||local-toggle|yes|yes|no|no|manual|command,skill
8
+ router-on||local-toggle|yes|yes|no|no|manual|command,skill
9
+ router-status||local-toggle|yes|yes|no|no|manual|command,skill
10
10
  router-session||prompt|yes|no|no|no|manual|command
11
- router-models|models|local-toggle|yes|no|no|no|manual|command
11
+ router-models|models|local-toggle|yes|yes|no|no|manual|command,skill
12
12
  disable-routing||local-toggle|no|yes|no|no|manual|skill
13
+ beta||prompt|yes|no|no|yes|manual|command