@workweave/router 0.2.9 → 0.2.11
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/README.md +31 -3
- package/cc-statusline.sh +325 -2
- package/codex-skills/force-model/SKILL.md +14 -0
- package/codex-skills/router-feedback/SKILL.md +14 -0
- package/codex-skills/unforce-model/SKILL.md +14 -0
- package/commands/models.md +46 -0
- package/commands/router-models.md +46 -0
- package/directives.tsv +12 -0
- package/install.sh +1477 -140
- package/package.json +6 -1
- package/pi-router/README.md +36 -7
- package/pi-router/skills/install-lsps/SKILL.md +75 -0
- package/pi-router/skills/lsp-guide/SKILL.md +63 -0
- package/pi-router/src/compaction.ts +46 -8
- package/pi-router/src/config.ts +34 -5
- package/pi-router/src/context-window.ts +12 -0
- package/pi-router/src/dispatch.ts +35 -2
- package/pi-router/src/index.ts +10 -1
- package/pi-router/src/lsp-broker.ts +255 -0
- package/pi-router/src/lsp-client.ts +435 -0
- package/pi-router/src/lsp-format.ts +230 -0
- package/pi-router/src/lsp-install.ts +215 -0
- package/pi-router/src/lsp-protocol.ts +128 -0
- package/pi-router/src/lsp-servers.ts +361 -0
- package/pi-router/src/lsp.ts +529 -0
- package/pi-router/src/pricing.generated.ts +6 -1
- package/pi-router/src/provider.ts +15 -2
- package/pi-router/src/routed-model.ts +17 -0
- package/registry.sh +100 -0
- package/uninstall.sh +173 -30
package/README.md
CHANGED
|
@@ -14,6 +14,22 @@ npx @workweave/router --base-url https://router.acme.internal
|
|
|
14
14
|
npx @workweave/router --non-interactive # reads $WEAVE_ROUTER_KEY, no prompts (defaults to claude)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Re-running the installer to pick up changes reuses the key already on disk, so
|
|
18
|
+
you paste it once and never again. `update` is the never-prompting form of that
|
|
19
|
+
(safe for cron; errors instead of asking when no key can be found):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx @workweave/router --claude # reuses the installed key
|
|
23
|
+
npx @workweave/router --claude --rotate-key # ignore it and prompt for a new one
|
|
24
|
+
npx @workweave/router update --claude # non-interactive refresh in place
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For Claude Code the installed statusline and `/force-model`, `/router-*` slash
|
|
28
|
+
commands also refresh themselves in the background about once a week (never
|
|
29
|
+
overwriting a wrapper you edited). Opt out with `WEAVE_STATUSLINE_UPDATE=0`, or
|
|
30
|
+
just the commands with `WEAVE_COMMANDS_UPDATE=0`. Codex, opencode, and pi have
|
|
31
|
+
no per-turn hook to refresh from — re-run the installer for those.
|
|
32
|
+
|
|
17
33
|
Version-pin for reproducible setups:
|
|
18
34
|
|
|
19
35
|
```bash
|
|
@@ -38,6 +54,19 @@ slash commands. The shell equivalent is `npx @workweave/router disable-routing`.
|
|
|
38
54
|
Cursor has no config file we own — toggle its base URL override in **Settings →
|
|
39
55
|
Models** instead.
|
|
40
56
|
|
|
57
|
+
Pick which models the router is allowed to route to:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @workweave/router models --claude # list every model, with its on/off state
|
|
61
|
+
npx @workweave/router models disable gpt-5.6 --claude # take one out of rotation
|
|
62
|
+
npx @workweave/router models enable gpt-5.6 --claude # put it back
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Inside Claude Code that's `/router-models` (alias `/models`). Editing needs a
|
|
66
|
+
router that serves the model-selection API; against the Weave-hosted router the
|
|
67
|
+
list still prints and points you at the dashboard, where model selection is an
|
|
68
|
+
organization-wide setting.
|
|
69
|
+
|
|
41
70
|
Uninstall:
|
|
42
71
|
|
|
43
72
|
```bash
|
|
@@ -66,9 +95,8 @@ Four install targets:
|
|
|
66
95
|
- **Codex** (`--codex`) — patches `~/.codex/config.toml` (or
|
|
67
96
|
`<repo>/.codex/config.toml`) with a managed `[model_providers.weave]`
|
|
68
97
|
block plus `model_provider = "weave"`. The provider preserves the existing
|
|
69
|
-
ChatGPT OAuth login.
|
|
70
|
-
|
|
71
|
-
their router's configured default because its HMM sidecar is optional. HMM or forced
|
|
98
|
+
ChatGPT OAuth login. No install pins `X-Weave-Router-Strategy`; every
|
|
99
|
+
endpoint keeps its router's configured default. HMM or forced
|
|
72
100
|
`gpt-5.6-sol`, `gpt-5.6-terra`, and `gpt-5.6-luna` turns use that plan;
|
|
73
101
|
every other selected model uses its WorkWeave deployment or BYOK credential.
|
|
74
102
|
The block lives between begin/end markers
|
package/cc-statusline.sh
CHANGED
|
@@ -125,6 +125,312 @@ weave_self_refresh() {
|
|
|
125
125
|
}
|
|
126
126
|
weave_self_refresh 2>/dev/null || true
|
|
127
127
|
|
|
128
|
+
# ---------- background slash-command refresh ----------
|
|
129
|
+
#
|
|
130
|
+
# The /force-model, /router-* … wrappers under <install>/.claude/commands/ are
|
|
131
|
+
# written once at install time and then never touched again, so a user who
|
|
132
|
+
# doesn't re-run the installer keeps whatever set shipped that day. This script
|
|
133
|
+
# is the only thing Claude Code invokes on every turn, so it doubles as the
|
|
134
|
+
# refresh point for them: same rate limit, same detached fork, same
|
|
135
|
+
# content-diff no-op as the self-refresh above.
|
|
136
|
+
#
|
|
137
|
+
# Never clobber a wrapper the user edited. The only way to tell an edit from a
|
|
138
|
+
# stale copy is to remember what we last downloaded, so each canonical file is
|
|
139
|
+
# cached (unrendered) per install under the user cache dir and a wrapper is
|
|
140
|
+
# replaced only when its bytes still match that baseline. With no baseline yet
|
|
141
|
+
# we can't prove anything, so the first run only seeds the cache and a swap can
|
|
142
|
+
# happen from the next one on. install.sh seeds it too, so fresh installs skip
|
|
143
|
+
# that warm-up round.
|
|
144
|
+
#
|
|
145
|
+
# Never touch a wrapper git tracks either. A project-scope install writes the
|
|
146
|
+
# wrappers into the repo's own .claude/commands/, and unlike cc-statusline.sh
|
|
147
|
+
# the installer does not gitignore them — so an unattended weekly rewrite would
|
|
148
|
+
# surface as unexplained dirty files (and could ride along in someone's commit).
|
|
149
|
+
# Only the installer changes tracked files, and only when a human runs it.
|
|
150
|
+
#
|
|
151
|
+
# Opt out with WEAVE_COMMANDS_UPDATE=0 (WEAVE_STATUSLINE_UPDATE=0 disables this
|
|
152
|
+
# too, along with every other network path here). Override the source with
|
|
153
|
+
# WEAVE_COMMANDS_URL_BASE=..., e.g. for self-hosters who fork.
|
|
154
|
+
|
|
155
|
+
# weave_command_tracked_by_git returns 0 when $1 is a file git tracks. Used to
|
|
156
|
+
# leave repo-committed wrappers alone; no git (or no repo) means untracked.
|
|
157
|
+
weave_command_tracked_by_git() {
|
|
158
|
+
command -v git >/dev/null 2>&1 || return 1
|
|
159
|
+
git -C "$(dirname "$1")" ls-files --error-unmatch -- "$1" >/dev/null 2>&1
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# weave_installed_command_names lists the wrappers this install may refresh.
|
|
163
|
+
# The statusline ships standalone (no registry.sh beside it), so the installed
|
|
164
|
+
# set — itself written from the registry — is the only source of truth here.
|
|
165
|
+
#
|
|
166
|
+
# Files written before ownership markers existed carry none, so matching on the
|
|
167
|
+
# marker alone would freeze every pre-marker install out of refreshes forever.
|
|
168
|
+
# List every wrapper instead and let the baseline comparison below decide: a
|
|
169
|
+
# file is replaced only when its bytes still match the last canonical copy, so
|
|
170
|
+
# a user-authored command is never touched whether or not it carries a marker.
|
|
171
|
+
weave_installed_command_names() {
|
|
172
|
+
local dir="$1" file
|
|
173
|
+
for file in "$dir"/*.md; do
|
|
174
|
+
[ -f "$file" ] || continue
|
|
175
|
+
printf '%s\n' "$(basename "$file" .md)"
|
|
176
|
+
done
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# weave_render_command prints $1 with the installer's {{SCOPE}} placeholder
|
|
180
|
+
# replaced by $2, matching how install_slash_commands writes the same file.
|
|
181
|
+
# Trailing newlines are stripped on both sides of every comparison below.
|
|
182
|
+
weave_render_command() {
|
|
183
|
+
local body
|
|
184
|
+
body="$(cat "$1" 2>/dev/null)" || return 1
|
|
185
|
+
printf '%s' "${body//\{\{SCOPE\}\}/$2}"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
weave_sync_commands() {
|
|
189
|
+
[ "${WEAVE_STATUSLINE_UPDATE:-1}" = "0" ] && return 0
|
|
190
|
+
[ "${WEAVE_COMMANDS_UPDATE:-1}" = "0" ] && return 0
|
|
191
|
+
command -v curl >/dev/null 2>&1 || return 0
|
|
192
|
+
|
|
193
|
+
local self="${BASH_SOURCE[0]:-$0}" self_dir cmd_dir
|
|
194
|
+
self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd -P)" || return 0
|
|
195
|
+
# User scope installs this script at <base>/.weave/, project and --dir at
|
|
196
|
+
# <base>/.claude/. Claude Code reads commands from <base>/.claude/commands in
|
|
197
|
+
# both layouts.
|
|
198
|
+
case "${self_dir##*/}" in
|
|
199
|
+
.weave) cmd_dir="${self_dir%/*}/.claude/commands" ;;
|
|
200
|
+
.claude) cmd_dir="$self_dir/commands" ;;
|
|
201
|
+
*) return 0 ;;
|
|
202
|
+
esac
|
|
203
|
+
[ -d "$cmd_dir" ] && [ -w "$cmd_dir" ] || return 0
|
|
204
|
+
|
|
205
|
+
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
|
|
206
|
+
local dir_slug
|
|
207
|
+
dir_slug="$(printf '%s' "$cmd_dir" | tr -c 'A-Za-z0-9._-' '_')"
|
|
208
|
+
# Baseline is keyed by install, not shared: two installs refresh on their own
|
|
209
|
+
# clocks, and a shared baseline updated by one would make the other's still-
|
|
210
|
+
# canonical wrappers look user-edited and freeze them forever.
|
|
211
|
+
local baseline_dir="$cache_dir/commands${dir_slug}"
|
|
212
|
+
local stamp="$cache_dir/checked-at${dir_slug}.commands"
|
|
213
|
+
mkdir -p "$baseline_dir" 2>/dev/null || return 0
|
|
214
|
+
|
|
215
|
+
local interval_days="${WEAVE_STATUSLINE_UPDATE_INTERVAL_DAYS:-7}"
|
|
216
|
+
local now stamp_mtime
|
|
217
|
+
now="$(date +%s 2>/dev/null)" || return 0
|
|
218
|
+
if [ -f "$stamp" ]; then
|
|
219
|
+
stamp_mtime="$(stat -c %Y "$stamp" 2>/dev/null || stat -f %m "$stamp" 2>/dev/null)" || stamp_mtime=0
|
|
220
|
+
else
|
|
221
|
+
stamp_mtime=0
|
|
222
|
+
fi
|
|
223
|
+
if [ -n "${stamp_mtime:-}" ] && [ "$stamp_mtime" -gt 0 ] \
|
|
224
|
+
&& [ $(( now - stamp_mtime )) -lt $(( interval_days * 86400 )) ]; then
|
|
225
|
+
return 0
|
|
226
|
+
fi
|
|
227
|
+
# Stamp before forking, same as the self-refresh: Claude Code calls us on
|
|
228
|
+
# every turn and concurrent invocations must not all start downloading.
|
|
229
|
+
: > "$stamp" 2>/dev/null || return 0
|
|
230
|
+
|
|
231
|
+
# router-off/on/status bake this install's scope selector into their npx
|
|
232
|
+
# line (upstream carries {{SCOPE}} there). Recover it from the installed copy
|
|
233
|
+
# so a project or --dir install keeps toggling its own config; when it can't
|
|
234
|
+
# be recovered those three are skipped rather than rewritten to point at the
|
|
235
|
+
# user-scope install.
|
|
236
|
+
local scope_args="" scope_known="false" off="$cmd_dir/router-off.md"
|
|
237
|
+
if [ -f "$off" ] && grep -q '^`npx @workweave/router off --claude.*`$' "$off" 2>/dev/null; then
|
|
238
|
+
scope_known="true"
|
|
239
|
+
scope_args="$(sed -n 's|^`npx @workweave/router off --claude\(.*\)`$|\1|p' "$off" | head -n 1)"
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
local url_base="${WEAVE_COMMANDS_URL_BASE:-https://raw.githubusercontent.com/workweave/router/main/install/commands}"
|
|
243
|
+
local name installed raw prev tmp new_body prev_body installed_body
|
|
244
|
+
(
|
|
245
|
+
# Detach stdin (CC pipes JSON to us) so curl can't consume it, and silence
|
|
246
|
+
# everything so no output leaks into the statusline.
|
|
247
|
+
exec </dev/null
|
|
248
|
+
while IFS= read -r name; do
|
|
249
|
+
installed="$cmd_dir/$name.md"
|
|
250
|
+
# Only ever refresh a wrapper that is already installed: a missing one
|
|
251
|
+
# was uninstalled or deliberately deleted, and resurrecting it would be
|
|
252
|
+
# a surprise. A symlink is user-owned; leave it alone. A git-tracked
|
|
253
|
+
# wrapper belongs to the repo — rewriting it would dirty a working tree
|
|
254
|
+
# nobody asked us to touch.
|
|
255
|
+
[ -f "$installed" ] || continue
|
|
256
|
+
[ -L "$installed" ] && continue
|
|
257
|
+
weave_command_tracked_by_git "$installed" && continue
|
|
258
|
+
|
|
259
|
+
raw="$baseline_dir/$name.md.tmp.$$"
|
|
260
|
+
curl -fsSL --max-time 15 "$url_base/$name.md" -o "$raw" 2>/dev/null || { rm -f "$raw"; continue; }
|
|
261
|
+
# Shape check: every wrapper opens with YAML front matter, so a 404 page
|
|
262
|
+
# or a truncated body can never be installed as a command.
|
|
263
|
+
if [ ! -s "$raw" ] || [ "$(head -n 1 "$raw")" != "---" ]; then
|
|
264
|
+
rm -f "$raw"
|
|
265
|
+
continue
|
|
266
|
+
fi
|
|
267
|
+
|
|
268
|
+
prev="$baseline_dir/$name.md"
|
|
269
|
+
if grep -q '{{SCOPE}}' "$raw" && [ "$scope_known" != "true" ]; then
|
|
270
|
+
mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
|
|
271
|
+
continue
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
if [ -f "$prev" ]; then
|
|
275
|
+
new_body="$(weave_render_command "$raw" "$scope_args")"
|
|
276
|
+
prev_body="$(weave_render_command "$prev" "$scope_args")"
|
|
277
|
+
installed_body="$(cat "$installed" 2>/dev/null | sed '/^<!-- weave-router managed command: .* -->$/d')" || installed_body=""
|
|
278
|
+
if [ "$prev_body" = "$installed_body" ] && [ "$new_body" != "$installed_body" ]; then
|
|
279
|
+
tmp="$installed.tmp.$$"
|
|
280
|
+
if printf '%s\n<!-- weave-router managed command: %s -->' "$new_body" "$name" >"$tmp" 2>/dev/null; then
|
|
281
|
+
mv "$tmp" "$installed" 2>/dev/null || rm -f "$tmp"
|
|
282
|
+
else
|
|
283
|
+
rm -f "$tmp"
|
|
284
|
+
fi
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
mv "$raw" "$prev" 2>/dev/null || rm -f "$raw"
|
|
288
|
+
done <<EOF
|
|
289
|
+
$(weave_installed_command_names "$cmd_dir")
|
|
290
|
+
EOF
|
|
291
|
+
) >/dev/null 2>&1 &
|
|
292
|
+
disown 2>/dev/null || true
|
|
293
|
+
return 0
|
|
294
|
+
}
|
|
295
|
+
weave_sync_commands 2>/dev/null || true
|
|
296
|
+
|
|
297
|
+
# ---------- org "hide terminal surfaces" gate ----------
|
|
298
|
+
#
|
|
299
|
+
# When the org has hidden the router's terminal surfaces, the statusline
|
|
300
|
+
# renders nothing: this prints no output and exits 0, leaving the slot blank.
|
|
301
|
+
# The setting comes from GET /v1/display-settings with the install's own
|
|
302
|
+
# router key, but the foreground path NEVER touches the network: it decides
|
|
303
|
+
# solely from a per-install cache file (TTL WEAVE_DISPLAY_SETTINGS_TTL_SECONDS,
|
|
304
|
+
# default 1h), and a missing or stale cache fails open — the statusline
|
|
305
|
+
# renders normally. When the cache is missing or stale a detached background
|
|
306
|
+
# refresh re-fetches the setting and rewrites the cache atomically, so the
|
|
307
|
+
# next turn picks up the fresh value; a refresh failure simply leaves the
|
|
308
|
+
# cache stale, which keeps failing open rather than pinning the gate closed.
|
|
309
|
+
#
|
|
310
|
+
# Claude Code runs the statusline every turn, so several invocations can see a
|
|
311
|
+
# stale cache at once. Letting each fetch independently is not safe: the
|
|
312
|
+
# responses can land out of order, and the loser's mv would replace a newer
|
|
313
|
+
# setting with an older one AND stamp it fresh, pinning the gate on a stale
|
|
314
|
+
# value for a full TTL. Refreshes are therefore serialized per cache key on a
|
|
315
|
+
# mkdir mutex (atomic everywhere; flock is absent on macOS), held across both
|
|
316
|
+
# the fetch and the write. A refresh that finds the mutex held exits rather
|
|
317
|
+
# than queueing — the in-flight one is at least as fresh as anything it would
|
|
318
|
+
# fetch, so waiting only to overwrite it is the bug. The one path that is not
|
|
319
|
+
# strictly exclusive is reclaiming a lock whose holder died; see below.
|
|
320
|
+
weave_hidden_gate() {
|
|
321
|
+
command -v curl >/dev/null 2>&1 || return 1
|
|
322
|
+
command -v jq >/dev/null 2>&1 || return 1
|
|
323
|
+
|
|
324
|
+
local cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/weave-router"
|
|
325
|
+
mkdir -p "$cache_dir" 2>/dev/null || return 1
|
|
326
|
+
local self="${BASH_SOURCE[0]:-$0}"
|
|
327
|
+
local script_slug
|
|
328
|
+
script_slug="$(printf '%s' "$self" | tr -c 'A-Za-z0-9._-' '_')"
|
|
329
|
+
local cache="$cache_dir/display-settings${script_slug}"
|
|
330
|
+
local ttl="${WEAVE_DISPLAY_SETTINGS_TTL_SECONDS:-3600}"
|
|
331
|
+
|
|
332
|
+
local now mtime fresh="false"
|
|
333
|
+
now="$(date +%s 2>/dev/null)" || now=0
|
|
334
|
+
if [ -f "$cache" ]; then
|
|
335
|
+
mtime="$(stat -c %Y "$cache" 2>/dev/null || stat -f %m "$cache" 2>/dev/null)" || mtime=0
|
|
336
|
+
if [ -n "${mtime:-}" ] && [ "$mtime" -gt 0 ] && [ $(( now - mtime )) -lt "$ttl" ]; then
|
|
337
|
+
fresh="true"
|
|
338
|
+
fi
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
# Foreground decision: only a fresh cache hides the surfaces. Anything else
|
|
342
|
+
# (no cache, stale cache, unreadable cache) renders normally so a slow or
|
|
343
|
+
# unreachable router can never stall a turn or wedge the statusline blank.
|
|
344
|
+
if [ "$fresh" = "true" ]; then
|
|
345
|
+
[ "$(cat "$cache" 2>/dev/null)" = "1" ]
|
|
346
|
+
return
|
|
347
|
+
fi
|
|
348
|
+
|
|
349
|
+
# Background refresh for the next invocation. Resolve the router base URL
|
|
350
|
+
# and key inside the subshell from the Claude Code settings the installer
|
|
351
|
+
# wrote: project/--dir installs put both under <base>/.claude alongside
|
|
352
|
+
# this script (key in settings.local.json), while a user-scope install
|
|
353
|
+
# lives under ~/.weave and reads ~/.claude. Resolve relative to the
|
|
354
|
+
# script's own location, falling back to user scope, so a project install
|
|
355
|
+
# never reads (or leaks) the user-scope key. ANTHROPIC_BASE_URL and
|
|
356
|
+
# WEAVE_ROUTER_BASE_URL may also be set in the environment. A file:// base
|
|
357
|
+
# URL is the offline/test seam: curl reads it as the response body
|
|
358
|
+
# directly, so the endpoint path is meaningless for it; real router URLs
|
|
359
|
+
# (https) get /v1/display-settings appended.
|
|
360
|
+
(
|
|
361
|
+
exec </dev/null
|
|
362
|
+
|
|
363
|
+
# Take the per-cache-key mutex, or bail. mkdir is the portable atomic
|
|
364
|
+
# test-and-set. A crashed holder would otherwise block refreshes forever,
|
|
365
|
+
# so a lock older than the fetch timeout is treated as abandoned and
|
|
366
|
+
# reclaimed. Releasing from a trap covers every exit path below.
|
|
367
|
+
lock="$cache.lock"
|
|
368
|
+
if ! mkdir "$lock" 2>/dev/null; then
|
|
369
|
+
lock_mtime="$(stat -c %Y "$lock" 2>/dev/null || stat -f %m "$lock" 2>/dev/null)" || lock_mtime=0
|
|
370
|
+
lock_now="$(date +%s 2>/dev/null)" || lock_now=0
|
|
371
|
+
if [ "${lock_mtime:-0}" -le 0 ] || [ $(( lock_now - lock_mtime )) -le 30 ]; then
|
|
372
|
+
exit 0
|
|
373
|
+
fi
|
|
374
|
+
# Reclaiming an abandoned lock must not be delete-then-recreate. With
|
|
375
|
+
# `rm -rf` + `mkdir`, refreshers that all see the same stale lock each
|
|
376
|
+
# delete the next one's freshly created directory, so many end up holding
|
|
377
|
+
# it at once (measured at 50 claimants: 4-11 concurrent holders) and their
|
|
378
|
+
# writes can land out of order. Renaming is atomic, so only one racer can
|
|
379
|
+
# move a given directory aside and the losers exit instead of clobbering
|
|
380
|
+
# the winner (same measurement: 1-2). It is not a perfect mutex — a
|
|
381
|
+
# straggler can still reclaim the new lock a winner just created, since
|
|
382
|
+
# nothing distinguishes it from the stale one — but real refreshes arrive
|
|
383
|
+
# one per turn rather than 50 at once, and the staleness threshold below
|
|
384
|
+
# is what bounds the rest.
|
|
385
|
+
dead="$lock.dead.$$"
|
|
386
|
+
mv "$lock" "$dead" 2>/dev/null || exit 0
|
|
387
|
+
rm -rf "$dead" 2>/dev/null
|
|
388
|
+
mkdir "$lock" 2>/dev/null || exit 0
|
|
389
|
+
fi
|
|
390
|
+
trap 'rmdir "$lock" 2>/dev/null' EXIT
|
|
391
|
+
|
|
392
|
+
self_dir="$(cd "$(dirname "$self")" 2>/dev/null && pwd)"
|
|
393
|
+
settings_base="$HOME"
|
|
394
|
+
case "$self_dir" in
|
|
395
|
+
*/.claude) settings_base="${self_dir%/.claude}" ;;
|
|
396
|
+
esac
|
|
397
|
+
settings="$settings_base/.claude/settings.json"
|
|
398
|
+
local_settings="$settings_base/.claude/settings.local.json"
|
|
399
|
+
base_url="${WEAVE_ROUTER_BASE_URL:-${ANTHROPIC_BASE_URL:-}}"
|
|
400
|
+
key="${WEAVE_ROUTER_KEY:-}"
|
|
401
|
+
if [ -z "$key" ] && [ -f "$settings" ]; then
|
|
402
|
+
key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$settings" 2>/dev/null | head -n1)"
|
|
403
|
+
fi
|
|
404
|
+
if [ -z "$key" ] && [ -f "$local_settings" ]; then
|
|
405
|
+
key="$(jq -r '.env.ANTHROPIC_CUSTOM_HEADERS // "" | split("\n")[] | select(startswith("X-Weave-Router-Key:")) | sub("^X-Weave-Router-Key:[[:space:]]*";"")' "$local_settings" 2>/dev/null | head -n1)"
|
|
406
|
+
fi
|
|
407
|
+
if [ -z "$base_url" ] && [ -f "$settings" ]; then
|
|
408
|
+
base_url="$(jq -r '.env.ANTHROPIC_BASE_URL // empty' "$settings" 2>/dev/null)"
|
|
409
|
+
fi
|
|
410
|
+
[ -n "$base_url" ] && [ -n "$key" ] || exit 0
|
|
411
|
+
url="${base_url%/}"
|
|
412
|
+
case "$url" in
|
|
413
|
+
file://*) ;;
|
|
414
|
+
*) url="$url/v1/display-settings" ;;
|
|
415
|
+
esac
|
|
416
|
+
body="$(curl -fsS --max-time 5 -H "X-Weave-Router-Key: $key" "$url" 2>/dev/null)" || exit 0
|
|
417
|
+
hidden="$(printf '%s' "$body" | jq -r '.hide_terminal_surfaces // false' 2>/dev/null)"
|
|
418
|
+
tmp="$cache.tmp.$$"
|
|
419
|
+
if [ "$hidden" = "true" ]; then
|
|
420
|
+
printf '1' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
|
|
421
|
+
else
|
|
422
|
+
printf '0' >"$tmp" 2>/dev/null && mv "$tmp" "$cache" 2>/dev/null
|
|
423
|
+
fi
|
|
424
|
+
rm -f "$tmp" 2>/dev/null
|
|
425
|
+
) >/dev/null 2>&1 &
|
|
426
|
+
disown 2>/dev/null || true
|
|
427
|
+
return 1
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if weave_hidden_gate </dev/null; then
|
|
431
|
+
exit 0
|
|
432
|
+
fi
|
|
433
|
+
|
|
128
434
|
input="$(cat)"
|
|
129
435
|
transcript_path="$(printf '%s' "$input" | jq -r '.transcript_path // empty')"
|
|
130
436
|
# Prefer model.id over display_name: pricing keys + the routed model id in
|
|
@@ -165,6 +471,7 @@ prices='{
|
|
|
165
471
|
"claude-sonnet-5": 0.003,
|
|
166
472
|
"deepseek/deepseek-v4-flash": 0.0001134,
|
|
167
473
|
"deepseek/deepseek-v4-pro": 0.00174,
|
|
474
|
+
"deepseek/deepseek-v4-pro-0813": 0.00174,
|
|
168
475
|
"gemini-2.0-flash": 0.0001,
|
|
169
476
|
"gemini-2.0-flash-lite": 0.000075,
|
|
170
477
|
"gemini-2.5-flash": 0.0003,
|
|
@@ -177,6 +484,7 @@ prices='{
|
|
|
177
484
|
"gemini-3.5-flash": 0.0015,
|
|
178
485
|
"gemini-3.5-flash-lite": 0.0003,
|
|
179
486
|
"gemini-3.6-flash": 0.0015,
|
|
487
|
+
"gemini-3.7-flash": 0.0015,
|
|
180
488
|
"google/gemma-4-26b-a4b-it": 0.00015,
|
|
181
489
|
"gpt-4.1": 0.002,
|
|
182
490
|
"gpt-4.1-mini": 0.0004,
|
|
@@ -196,7 +504,9 @@ prices='{
|
|
|
196
504
|
"gpt-5.5-nano": 0.00015,
|
|
197
505
|
"gpt-5.5-pro": 0.03,
|
|
198
506
|
"gpt-5.6-luna": 0.001,
|
|
507
|
+
"gpt-5.6-luna-pro": 0.001,
|
|
199
508
|
"gpt-5.6-sol": 0.005,
|
|
509
|
+
"gpt-5.6-sol-pro": 0.005,
|
|
200
510
|
"gpt-5.6-terra": 0.0025,
|
|
201
511
|
"grok-4.5": 0.002,
|
|
202
512
|
"grok-4.6": 0.002,
|
|
@@ -219,7 +529,8 @@ prices='{
|
|
|
219
529
|
"xiaomi/mimo-v2.5-pro": 0.001,
|
|
220
530
|
"z-ai/glm-5": 0.001,
|
|
221
531
|
"z-ai/glm-5.1": 0.0014,
|
|
222
|
-
"z-ai/glm-5.2": 0.0014
|
|
532
|
+
"z-ai/glm-5.2": 0.0014,
|
|
533
|
+
"z-ai/glm-5.3-flash": 0.00015
|
|
223
534
|
},
|
|
224
535
|
"output": {
|
|
225
536
|
"claude-fable-5": 0.05,
|
|
@@ -236,6 +547,7 @@ prices='{
|
|
|
236
547
|
"claude-sonnet-5": 0.015,
|
|
237
548
|
"deepseek/deepseek-v4-flash": 0.0002791,
|
|
238
549
|
"deepseek/deepseek-v4-pro": 0.00348,
|
|
550
|
+
"deepseek/deepseek-v4-pro-0813": 0.00348,
|
|
239
551
|
"gemini-2.0-flash": 0.0004,
|
|
240
552
|
"gemini-2.0-flash-lite": 0.0003,
|
|
241
553
|
"gemini-2.5-flash": 0.0012,
|
|
@@ -248,6 +560,7 @@ prices='{
|
|
|
248
560
|
"gemini-3.5-flash": 0.009,
|
|
249
561
|
"gemini-3.5-flash-lite": 0.0025,
|
|
250
562
|
"gemini-3.6-flash": 0.0075,
|
|
563
|
+
"gemini-3.7-flash": 0.0075,
|
|
251
564
|
"google/gemma-4-26b-a4b-it": 0.0006,
|
|
252
565
|
"gpt-4.1": 0.008,
|
|
253
566
|
"gpt-4.1-mini": 0.0016,
|
|
@@ -267,7 +580,9 @@ prices='{
|
|
|
267
580
|
"gpt-5.5-nano": 0.0006,
|
|
268
581
|
"gpt-5.5-pro": 0.18,
|
|
269
582
|
"gpt-5.6-luna": 0.006,
|
|
583
|
+
"gpt-5.6-luna-pro": 0.006,
|
|
270
584
|
"gpt-5.6-sol": 0.03,
|
|
585
|
+
"gpt-5.6-sol-pro": 0.03,
|
|
271
586
|
"gpt-5.6-terra": 0.015,
|
|
272
587
|
"grok-4.5": 0.006,
|
|
273
588
|
"grok-4.6": 0.006,
|
|
@@ -290,7 +605,8 @@ prices='{
|
|
|
290
605
|
"xiaomi/mimo-v2.5-pro": 0.003,
|
|
291
606
|
"z-ai/glm-5": 0.0032,
|
|
292
607
|
"z-ai/glm-5.1": 0.0044,
|
|
293
|
-
"z-ai/glm-5.2": 0.0044
|
|
608
|
+
"z-ai/glm-5.2": 0.0044,
|
|
609
|
+
"z-ai/glm-5.3-flash": 0.0005
|
|
294
610
|
}
|
|
295
611
|
}'
|
|
296
612
|
# END_GENERATED_PRICES
|
|
@@ -345,6 +661,12 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
345
661
|
# * unrecognized model → "… isn't a recognized model · keeping
|
|
346
662
|
# automatic routing" — a NO-OP: the prior
|
|
347
663
|
# pin, if any, is left untouched
|
|
664
|
+
# * listing (historical) → "… pick a model by id …" / "no models are
|
|
665
|
+
# available to pin …" — also NO-OPs. The
|
|
666
|
+
# router no longer emits these, but a
|
|
667
|
+
# transcript written while it did still
|
|
668
|
+
# carries them, and they must not read as
|
|
669
|
+
# a clear or a live pin loses its [forced]
|
|
348
670
|
# These persist on disk (the ingress stripper only scrubs them from upstream
|
|
349
671
|
# requests). Classify each weave-router turn newest-first, skip the no-op
|
|
350
672
|
# "rejected" acks, and let the latest real state change decide: an "applied"
|
|
@@ -358,6 +680,7 @@ if [[ -n "$transcript_path" && -f "$transcript_path" ]]; then
|
|
|
358
680
|
| ([.message.content[]? | select(.type? == "text") | .text] | join(" ") | gsub("[\n\r]"; " ")) as $t
|
|
359
681
|
| if ($t | test("force-model applied:")) then "APPLIED " + ($t | capture("force-model applied: (?<m>[^ ]+)").m)
|
|
360
682
|
elif ($t | test("isn.t a recognized model")) then "REJECTED"
|
|
683
|
+
elif ($t | test("pick a model by id|no models are available to pin")) then "REJECTED"
|
|
361
684
|
else "CLEARED" end' 2>/dev/null \
|
|
362
685
|
| grep -m1 -v '^REJECTED$' || true)"
|
|
363
686
|
if [[ "$force_state" == APPLIED\ * ]]; then
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: force-model
|
|
3
|
+
description: "Pin this Codex session to a specific model through the Weave Router."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- weave-router managed force-model skill -->
|
|
7
|
+
|
|
8
|
+
When the user invokes `$force-model <model-id>` (or asks to use the `$fm` alias), send a normal user message whose first character is one literal space, followed by `/force-model ` and the requested model id. Do not use a Codex slash command and do not omit the leading space. For example, send exactly:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
/force-model gpt-5.6-terra
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Preserve any model id exactly as provided. Report the router's response after it returns.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: router-feedback
|
|
3
|
+
description: "Submit feedback about a Weave Router decision or model performance."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- weave-router managed router-feedback skill -->
|
|
7
|
+
|
|
8
|
+
When the user invokes `$router-feedback <feedback>` (or asks to use the `$rf` alias), send a normal user message whose first character is one literal space, followed by `/router-feedback ` and the feedback text. Do not use a Codex slash command and do not omit the leading space. Preserve the feedback text exactly. For example:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
/router-feedback the selected model struggled with this task
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Report the router's response after it returns.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unforce-model
|
|
3
|
+
description: "Clear the active Weave Router model pin for this Codex session."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- weave-router managed unforce-model skill -->
|
|
7
|
+
|
|
8
|
+
When the user invokes `$unforce-model` (or asks to use the `$ufm` alias), send a normal user message whose first character is one literal space, followed by `/unforce-model`. Do not use a Codex slash command and do not omit the leading space. Send exactly:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
/unforce-model
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Report the router's response after it returns.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Alias for /router-models — list the models the Weave Router may route to, and turn them on or off.
|
|
3
|
+
argument-hint: [model or provider to enable/disable]
|
|
4
|
+
allowed-tools: Bash(npx:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Show me which models this installation lets the Weave Router pick from, and
|
|
8
|
+
change that selection when I ask. This is the same list — and the same stored
|
|
9
|
+
setting — as the checkboxes on the router dashboard's settings page.
|
|
10
|
+
|
|
11
|
+
Start by running:
|
|
12
|
+
|
|
13
|
+
`npx @workweave/router models --claude{{SCOPE}}`
|
|
14
|
+
|
|
15
|
+
That prints every deployed model grouped by provider, with `[x]` for models the
|
|
16
|
+
router may pick and `[ ]` for models it may not. Present it back to me as a
|
|
17
|
+
compact checklist in that same `[x]` / `[ ]` form, keeping the provider
|
|
18
|
+
grouping and the exact model ids — I select models by id.
|
|
19
|
+
|
|
20
|
+
Then:
|
|
21
|
+
|
|
22
|
+
- If I named models or providers in `$ARGUMENTS`, work out whether I want them
|
|
23
|
+
on or off from how I phrased it, and apply it with
|
|
24
|
+
`npx @workweave/router models enable <id>... --claude{{SCOPE}}` or
|
|
25
|
+
`npx @workweave/router models disable <id>... --claude{{SCOPE}}` (add
|
|
26
|
+
`providers` before `enable`/`disable` to switch a whole provider). Several
|
|
27
|
+
ids can go in one call. Then re-run the list and show me the result.
|
|
28
|
+
- If I named nothing, stop after the list and ask which ones I want to change.
|
|
29
|
+
Don't change anything I didn't ask for.
|
|
30
|
+
|
|
31
|
+
Other things I might ask for:
|
|
32
|
+
|
|
33
|
+
- Rank models by preference:
|
|
34
|
+
`npx @workweave/router models prefer <id> <id>... --claude{{SCOPE}}` (order
|
|
35
|
+
matters), or `npx @workweave/router models prefer clear --claude{{SCOPE}}` to
|
|
36
|
+
drop the ranking.
|
|
37
|
+
- Providers only: `npx @workweave/router models providers --claude{{SCOPE}}`.
|
|
38
|
+
|
|
39
|
+
If the command reports that this router doesn't expose model selection, that's
|
|
40
|
+
a Weave-hosted router: model selection belongs to the whole organization there,
|
|
41
|
+
so tell me to change it at https://router.workweave.ai/dashboard/settings —
|
|
42
|
+
don't try to work around it. Its listing carries no on/off state, so present it
|
|
43
|
+
as a plain list; don't infer which models are enabled.
|
|
44
|
+
|
|
45
|
+
Disabling a model takes effect on the router's next routing decision; no
|
|
46
|
+
restart is needed.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: List the models the Weave Router may route to, and turn them on or off.
|
|
3
|
+
argument-hint: [model or provider to enable/disable]
|
|
4
|
+
allowed-tools: Bash(npx:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Show me which models this installation lets the Weave Router pick from, and
|
|
8
|
+
change that selection when I ask. This is the same list — and the same stored
|
|
9
|
+
setting — as the checkboxes on the router dashboard's settings page.
|
|
10
|
+
|
|
11
|
+
Start by running:
|
|
12
|
+
|
|
13
|
+
`npx @workweave/router models --claude{{SCOPE}}`
|
|
14
|
+
|
|
15
|
+
That prints every deployed model grouped by provider, with `[x]` for models the
|
|
16
|
+
router may pick and `[ ]` for models it may not. Present it back to me as a
|
|
17
|
+
compact checklist in that same `[x]` / `[ ]` form, keeping the provider
|
|
18
|
+
grouping and the exact model ids — I select models by id.
|
|
19
|
+
|
|
20
|
+
Then:
|
|
21
|
+
|
|
22
|
+
- If I named models or providers in `$ARGUMENTS`, work out whether I want them
|
|
23
|
+
on or off from how I phrased it, and apply it with
|
|
24
|
+
`npx @workweave/router models enable <id>... --claude{{SCOPE}}` or
|
|
25
|
+
`npx @workweave/router models disable <id>... --claude{{SCOPE}}` (add
|
|
26
|
+
`providers` before `enable`/`disable` to switch a whole provider). Several
|
|
27
|
+
ids can go in one call. Then re-run the list and show me the result.
|
|
28
|
+
- If I named nothing, stop after the list and ask which ones I want to change.
|
|
29
|
+
Don't change anything I didn't ask for.
|
|
30
|
+
|
|
31
|
+
Other things I might ask for:
|
|
32
|
+
|
|
33
|
+
- Rank models by preference:
|
|
34
|
+
`npx @workweave/router models prefer <id> <id>... --claude{{SCOPE}}` (order
|
|
35
|
+
matters), or `npx @workweave/router models prefer clear --claude{{SCOPE}}` to
|
|
36
|
+
drop the ranking.
|
|
37
|
+
- Providers only: `npx @workweave/router models providers --claude{{SCOPE}}`.
|
|
38
|
+
|
|
39
|
+
If the command reports that this router doesn't expose model selection, that's
|
|
40
|
+
a Weave-hosted router: model selection belongs to the whole organization there,
|
|
41
|
+
so tell me to change it at https://router.workweave.ai/dashboard/settings —
|
|
42
|
+
don't try to work around it. Its listing carries no on/off state, so present it
|
|
43
|
+
as a plain list; don't infer which models are enabled.
|
|
44
|
+
|
|
45
|
+
Disabling a model takes effect on the router's next routing decision; no
|
|
46
|
+
restart is needed.
|
package/directives.tsv
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Weave Router directive registry
|
|
2
|
+
# canonical|aliases|capability|claude|codex|opencode|pi|cursor|adapter
|
|
3
|
+
# aliases are comma-separated; client columns are yes/no. adapter is the native asset kind.
|
|
4
|
+
force-model|fm|prompt|yes|yes|yes|yes|manual|command,skill
|
|
5
|
+
unforce-model|ufm|prompt|yes|yes|yes|yes|manual|command,skill
|
|
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
|
|
10
|
+
router-session||prompt|yes|no|no|no|manual|command
|
|
11
|
+
router-models|models|local-toggle|yes|no|no|no|manual|command
|
|
12
|
+
disable-routing||local-toggle|no|yes|no|no|manual|skill
|