mobygate 0.9.2 → 0.9.5
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/CHANGELOG.md +108 -0
- package/dashboard.css +1 -0
- package/index.html +1 -15
- package/package.json +3 -2
- package/server.js +71 -41
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,114 @@ All notable changes to mobygate are documented here. Format loosely follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version numbers are
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.9.5] — 2026-06-17
|
|
8
|
+
|
|
9
|
+
Claude Agent SDK upgrade: `0.2.112` → `0.3.181`. Pure dependency bump —
|
|
10
|
+
no mobygate behavior change. Closes a 58-release gap and re-aligns the
|
|
11
|
+
SDK with the current `claude` CLI generation (CLI 2.1.181 ↔ SDK 0.3.181
|
|
12
|
+
ship in lockstep).
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- **`@anthropic-ai/claude-agent-sdk` `^0.2.112` → `^0.3.181`.** Resolves
|
|
17
|
+
alongside `@anthropic-ai/sdk@0.104.2`. Lockfile regenerated.
|
|
18
|
+
|
|
19
|
+
### Verification
|
|
20
|
+
|
|
21
|
+
Vetted on an isolated worktree before merge:
|
|
22
|
+
|
|
23
|
+
- **Static surface diff (0.2.112 vs 0.3.181):** every SDK API mobygate
|
|
24
|
+
depends on is identical — `query()` signature, the
|
|
25
|
+
`{ type:'preset', preset:'claude_code', append }` systemPrompt shape,
|
|
26
|
+
`permissionMode`, `allowDangerouslySkipPermissions`, `persistSession`,
|
|
27
|
+
`resume`, `betas`, `mcpServers`. Nothing removed or reshaped.
|
|
28
|
+
- **Boot:** server starts, binds, `/health` 200, no import errors.
|
|
29
|
+
- **Smoke suite 11/11** against the real API on 0.3.181 — including
|
|
30
|
+
every runtime path: `/v1/messages` and `/v1/chat/completions`
|
|
31
|
+
(non-streaming, streaming, tools), `/quiet` scrub, and the SQLite
|
|
32
|
+
capture index.
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
|
|
36
|
+
- The SDK reads OAuth from the macOS Keychain (`Claude Code-credentials`),
|
|
37
|
+
not `~/.claude/.credentials.json` — the latter is a stale secondary
|
|
38
|
+
cache and can read "expired" even when auth is healthy. If mobygate
|
|
39
|
+
ever 401s with "proxy refresh failed", re-auth via `claude` → `/login`
|
|
40
|
+
(renews the Keychain credential); restarting mobygate alone won't fix
|
|
41
|
+
a genuinely expired token.
|
|
42
|
+
|
|
43
|
+
## [0.9.4] — 2026-05-29
|
|
44
|
+
|
|
45
|
+
Fable 5 support. Anthropic shipped the Fable 5 model family (parallel to
|
|
46
|
+
the Opus 4.x line); this adds it to the model map so `claude-fable-5`
|
|
47
|
+
(and the `fable` alias) resolve to the 1M-context variant instead of
|
|
48
|
+
silently falling back to the default.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **Fable 5 model map entries:** `claude-fable-5`, `claude-fable-5[1m]`,
|
|
53
|
+
`claude-fable-5-1m`, `claude-fable-5-200k`, plus the `fable` /
|
|
54
|
+
`fable-200k` short aliases. The bare `claude-fable-5` and `fable`
|
|
55
|
+
aliases route to `claude-fable-5[1m]` (1M context, verified
|
|
56
|
+
Max-included — the probe ran on Claude Max OAuth with no extra-usage
|
|
57
|
+
rejection). Use `claude-fable-5-200k` for the standard 200k variant.
|
|
58
|
+
|
|
59
|
+
### Notes
|
|
60
|
+
|
|
61
|
+
- Additive only. Opus 4.8 stays the default model; Fable resolves only
|
|
62
|
+
when explicitly requested. All Opus / Sonnet / Haiku entries unchanged.
|
|
63
|
+
- Verified live 2026-05-29 against Anthropic via the `claude` CLI:
|
|
64
|
+
`claude-fable-5` accepted and self-identifies; `claude-fable-9`
|
|
65
|
+
(control) rejected as nonexistent.
|
|
66
|
+
|
|
67
|
+
## [0.9.3] — 2026-05-29
|
|
68
|
+
|
|
69
|
+
Opus 4.8 support + control-plane Host-header hardening.
|
|
70
|
+
|
|
71
|
+
Two threads land together. (1) Anthropic shipped Opus 4.8; mobygate was
|
|
72
|
+
silently downgrading every `claude-opus-4-8` request to `4-7[1m]` because
|
|
73
|
+
the alias wasn't in the model map — so clients (including Taste, which
|
|
74
|
+
already targets 4-8) were unknowingly running on the older model. (2) A
|
|
75
|
+
batch of sensitive control-plane GET endpoints were reachable from
|
|
76
|
+
non-local Host headers (DNS-rebinding / CSRF surface).
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Opus 4.8 model map entries:** `claude-opus-4-8`,
|
|
81
|
+
`claude-opus-4-8[1m]`, `claude-opus-4-8-1m`, `claude-opus-4-8-200k`.
|
|
82
|
+
Bare `claude-opus-4-8`, the `opus` alias, and the catch-all
|
|
83
|
+
`claude-opus-4` now resolve to `claude-opus-4-8[1m]` (1M, Max-included
|
|
84
|
+
— verified live). All `claude-opus-4-7*` entries retained so explicit
|
|
85
|
+
4-7 requests still resolve correctly.
|
|
86
|
+
- **`DEFAULT_MODEL` bumped** `claude-opus-4-7[1m]` → `claude-opus-4-8[1m]`.
|
|
87
|
+
|
|
88
|
+
### Changed (security)
|
|
89
|
+
|
|
90
|
+
- **`requireLocalOrigin` now guards** `/sessions`, `/sessions/:key`,
|
|
91
|
+
`/dashboard/recent`, `/dashboard/sessions`, `/auth/status`,
|
|
92
|
+
`/update/check`, `/update/status`. These previously answered requests
|
|
93
|
+
with hostile `Host` headers, exposing session IDs, dashboard event
|
|
94
|
+
metadata, auth state, and update logs to DNS-rebinding / CSRF.
|
|
95
|
+
- **Extracted `serializeSession()`** — collapses the duplicated
|
|
96
|
+
session-JSON formatting that the `/sessions` routes and dashboard
|
|
97
|
+
shared into one helper (dashboard vs API shape via an option flag).
|
|
98
|
+
|
|
99
|
+
### Added (dashboard)
|
|
100
|
+
|
|
101
|
+
- **`GET /dashboard.css`** serves the bundled dashboard stylesheet
|
|
102
|
+
(extracted from inline `index.html`), with no-cache headers so edits
|
|
103
|
+
show on reload. Added `dashboard.css` to the published `files[]`.
|
|
104
|
+
- **`test/smoke.test.mjs`** gains a raw-`node:http` case that spoofs
|
|
105
|
+
`Host: evil.example` to assert the guarded endpoints reject it. Full
|
|
106
|
+
suite: 11/11 passing.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- **Silent model downgrade.** A `claude-opus-4-8` request returned
|
|
111
|
+
`model: claude-opus-4-7[1m]` in the response body before this fix.
|
|
112
|
+
Confirmed in a real capture (requested 4-8, ran as 4-7) — the bug had
|
|
113
|
+
been quietly affecting live Claude Code and Taste traffic.
|
|
114
|
+
|
|
7
115
|
## [0.9.2] — 2026-05-03
|
|
8
116
|
|
|
9
117
|
Return `405 Method Not Allowed` (with `Allow: POST` header) for `GET`
|
package/dashboard.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.z-50{z-index:50}.m-0{margin:0}.mx-auto{margin-left:auto;margin-right:auto}.-ml-3{margin-left:-.75rem}.mt-1{margin-top:.25rem}.mt-10{margin-top:2.5rem}.mt-auto{margin-top:auto}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-\[110px\]{height:110px}.h-\[22px\]{height:22px}.h-full{height:100%}.max-h-\[180px\]{max-height:180px}.max-h-\[240px\]{max-height:240px}.max-h-\[40vh\]{max-height:40vh}.max-h-\[52vh\]{max-height:52vh}.max-h-\[70vh\]{max-height:70vh}.min-h-screen{min-height:100vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-\[100px\]{width:100px}.w-\[110px\]{width:110px}.w-\[160px\]{width:160px}.w-\[180px\]{width:180px}.w-\[20\%\]{width:20%}.w-\[60px\]{width:60px}.w-\[6px\]{width:6px}.w-\[70px\]{width:70px}.w-\[72px\]{width:72px}.w-\[80px\]{width:80px}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0}.max-w-3xl{max-width:48rem}.max-w-\[1440px\]{max-width:1440px}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.basis-0{flex-basis:0px}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-\[22px\]{gap:22px}.gap-\[3px\]{gap:3px}.gap-\[5px\]{gap:5px}.overflow-auto{overflow:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded-full{border-radius:9999px}.rounded-md{border-radius:.375rem}.rounded-r-md{border-top-right-radius:.375rem;border-bottom-right-radius:.375rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-\[\#1A1A15\]{--tw-border-opacity:1;border-color:rgb(26 26 21/var(--tw-border-opacity,1))}.border-\[\#2A2A1F\]{--tw-border-opacity:1;border-color:rgb(42 42 31/var(--tw-border-opacity,1))}.border-\[\#B7E56D\]{--tw-border-opacity:1;border-color:rgb(183 229 109/var(--tw-border-opacity,1))}.border-l-\[\#4EA4C4\]{--tw-border-opacity:1;border-left-color:rgb(78 164 196/var(--tw-border-opacity,1))}.border-l-\[\#B7E56D\]{--tw-border-opacity:1;border-left-color:rgb(183 229 109/var(--tw-border-opacity,1))}.border-l-\[\#E89B2E\]{--tw-border-opacity:1;border-left-color:rgb(232 155 46/var(--tw-border-opacity,1))}.bg-\[\#0B0B09\]{--tw-bg-opacity:1;background-color:rgb(11 11 9/var(--tw-bg-opacity,1))}.bg-\[\#121210\]{--tw-bg-opacity:1;background-color:rgb(18 18 16/var(--tw-bg-opacity,1))}.bg-\[\#1A1A15\]{--tw-bg-opacity:1;background-color:rgb(26 26 21/var(--tw-bg-opacity,1))}.bg-\[\#2A2A1F\]{--tw-bg-opacity:1;background-color:rgb(42 42 31/var(--tw-bg-opacity,1))}.bg-\[\#4EA4C4\]{--tw-bg-opacity:1;background-color:rgb(78 164 196/var(--tw-bg-opacity,1))}.bg-\[\#5A5F54\]{--tw-bg-opacity:1;background-color:rgb(90 95 84/var(--tw-bg-opacity,1))}.bg-\[\#B7E56D1A\]{background-color:#b7e56d1a}.bg-\[\#B7E56D\]{--tw-bg-opacity:1;background-color:rgb(183 229 109/var(--tw-bg-opacity,1))}.bg-\[\#E89B2E\]{--tw-bg-opacity:1;background-color:rgb(232 155 46/var(--tw-bg-opacity,1))}.bg-black\/70{background-color:rgba(0,0,0,.7)}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-3\.5{padding-left:.875rem;padding-right:.875rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-\[14px\]{padding-top:14px;padding-bottom:14px}.py-\[18px\]{padding-top:18px;padding-bottom:18px}.py-\[22px\]{padding-top:22px;padding-bottom:22px}.py-\[3px\]{padding-top:3px;padding-bottom:3px}.pb-2{padding-bottom:.5rem}.pb-7{padding-bottom:1.75rem}.pr-1{padding-right:.25rem}.pt-2{padding-top:.5rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-right{text-align:right}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[64px\]{font-size:64px}.text-\[9px\]{font-size:9px}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.leading-3{line-height:.75rem}.leading-4{line-height:1rem}.leading-8{line-height:2rem}.leading-\[14px\]{line-height:14px}.leading-\[15px\]{line-height:15px}.leading-\[16px\]{line-height:16px}.leading-\[18px\]{line-height:18px}.leading-\[56px\]{line-height:56px}.tracking-\[0\.04em\]{letter-spacing:.04em}.tracking-\[0\.06em\]{letter-spacing:.06em}.tracking-\[0\.08em\]{letter-spacing:.08em}.tracking-\[0\.12em\]{letter-spacing:.12em}.tracking-\[0\.14em\]{letter-spacing:.14em}.tracking-\[0\.18em\]{letter-spacing:.18em}.tracking-\[0\.22em\]{letter-spacing:.22em}.tracking-widest{letter-spacing:.1em}.text-\[\#0B0B09\]{--tw-text-opacity:1;color:rgb(11 11 9/var(--tw-text-opacity,1))}.text-\[\#4EA4C4\]{--tw-text-opacity:1;color:rgb(78 164 196/var(--tw-text-opacity,1))}.text-\[\#5A5F54\]{--tw-text-opacity:1;color:rgb(90 95 84/var(--tw-text-opacity,1))}.text-\[\#8A9A6A\]{--tw-text-opacity:1;color:rgb(138 154 106/var(--tw-text-opacity,1))}.text-\[\#B7E56D\]{--tw-text-opacity:1;color:rgb(183 229 109/var(--tw-text-opacity,1))}.text-\[\#C9D9A8\]{--tw-text-opacity:1;color:rgb(201 217 168/var(--tw-text-opacity,1))}.text-\[\#E89B2E\]{--tw-text-opacity:1;color:rgb(232 155 46/var(--tw-text-opacity,1))}.text-\[\#F3EFE4\]{--tw-text-opacity:1;color:rgb(243 239 228/var(--tw-text-opacity,1))}.underline{text-decoration-line:underline}.decoration-dotted{text-decoration-style:dotted}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.accent-\[\#4EA4C4\]{accent-color:#4ea4c4}.opacity-50{opacity:.5}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:border-\[\#5A5F54\]:hover{--tw-border-opacity:1;border-color:rgb(90 95 84/var(--tw-border-opacity,1))}.hover\:bg-\[\#1A1F12\]:hover{--tw-bg-opacity:1;background-color:rgb(26 31 18/var(--tw-bg-opacity,1))}.hover\:text-\[\#C9D9A8\]:hover{--tw-text-opacity:1;color:rgb(201 217 168/var(--tw-text-opacity,1))}.hover\:brightness-110:hover{--tw-brightness:brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}
|
package/index.html
CHANGED
|
@@ -7,21 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=VT323&display=swap" rel="stylesheet">
|
|
10
|
-
<
|
|
11
|
-
<script>
|
|
12
|
-
/* Design tokens transcribed from the Paper artboard C1-0.
|
|
13
|
-
Using arbitrary Tailwind values for exact fidelity with the design. */
|
|
14
|
-
tailwind.config = {
|
|
15
|
-
theme: {
|
|
16
|
-
extend: {
|
|
17
|
-
fontFamily: {
|
|
18
|
-
display: ['VT323', 'system-ui', 'monospace'],
|
|
19
|
-
mono: ['"JetBrains Mono"', 'ui-monospace', 'SFMono-Regular', 'Menlo', 'monospace'],
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
</script>
|
|
10
|
+
<link rel="stylesheet" href="/dashboard.css">
|
|
25
11
|
<style>
|
|
26
12
|
html, body { background: #0B0B09; color: #F3EFE4; font-family: 'JetBrains Mono', ui-monospace, Menlo, monospace; }
|
|
27
13
|
.whale-ascii {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobygate",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "OpenAI-compatible local proxy for Claude Max. The Möbius-strip gateway: OpenAI shape in, Claude Max out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"postinstall": "node scripts/postinstall.js || true"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@anthropic-ai/claude-agent-sdk": "^0.
|
|
21
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.181",
|
|
22
22
|
"better-sqlite3": "^12.9.0",
|
|
23
23
|
"express": "^5.1.0",
|
|
24
24
|
"js-yaml": "^4.1.1",
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"launchd",
|
|
61
61
|
"server.js",
|
|
62
62
|
"index.html",
|
|
63
|
+
"dashboard.css",
|
|
63
64
|
"inspector.html",
|
|
64
65
|
"mcp-inspect.mjs",
|
|
65
66
|
"README.md",
|
package/server.js
CHANGED
|
@@ -87,7 +87,7 @@ const PORT = parseInt(process.env.PORT || '3456', 10);
|
|
|
87
87
|
// want to share the proxy on a network can set bind: 0.0.0.0 (or a specific
|
|
88
88
|
// interface) in ~/.mobygate/config.yaml, but should add auth in front of it.
|
|
89
89
|
const BIND = process.env.BIND || '127.0.0.1';
|
|
90
|
-
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'claude-opus-4-
|
|
90
|
+
const DEFAULT_MODEL = process.env.DEFAULT_MODEL || 'claude-opus-4-8[1m]';
|
|
91
91
|
// SESSION_TTL_MS: how long mobygate holds onto an idle SDK session before
|
|
92
92
|
// expiring it from its in-memory + on-disk session store. v0.8.5 raises
|
|
93
93
|
// the default from 1h → 4h based on real-world usage data: most multi-
|
|
@@ -203,7 +203,13 @@ for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
|
|
|
203
203
|
// falling back to opus or returning a zero-billed response. Fixed in
|
|
204
204
|
// v0.8.2 by routing 4-6 through directly.
|
|
205
205
|
const MODEL_MAP = {
|
|
206
|
-
|
|
206
|
+
// Latest opus → 4-8 (1M, Max-included — verified live 2026-05-29).
|
|
207
|
+
// 4-7 entries kept so explicit 4-7 requests still resolve.
|
|
208
|
+
'claude-opus-4': 'claude-opus-4-8[1m]',
|
|
209
|
+
'claude-opus-4-8': 'claude-opus-4-8[1m]',
|
|
210
|
+
'claude-opus-4-8[1m]': 'claude-opus-4-8[1m]',
|
|
211
|
+
'claude-opus-4-8-1m': 'claude-opus-4-8[1m]',
|
|
212
|
+
'claude-opus-4-8-200k': 'claude-opus-4-8',
|
|
207
213
|
'claude-opus-4-6': 'claude-opus-4-6',
|
|
208
214
|
'claude-opus-4-7': 'claude-opus-4-7[1m]',
|
|
209
215
|
'claude-opus-4-7[1m]': 'claude-opus-4-7[1m]',
|
|
@@ -222,7 +228,16 @@ const MODEL_MAP = {
|
|
|
222
228
|
'claude-sonnet-4-6-200k': 'claude-sonnet-4-6', // explicit 200k alias (redundant, kept for clarity)
|
|
223
229
|
'claude-haiku-4': 'claude-haiku-4-5-20251001',
|
|
224
230
|
'claude-haiku-4-5': 'claude-haiku-4-5-20251001',
|
|
225
|
-
|
|
231
|
+
// Fable 5 — distinct recent model family (parallel to Opus 4.x).
|
|
232
|
+
// 1M variant Max-included (verified live 2026-05-29). Additive: opus
|
|
233
|
+
// stays the default; fable resolves only when explicitly requested.
|
|
234
|
+
'claude-fable-5': 'claude-fable-5[1m]',
|
|
235
|
+
'claude-fable-5[1m]': 'claude-fable-5[1m]',
|
|
236
|
+
'claude-fable-5-1m': 'claude-fable-5[1m]',
|
|
237
|
+
'claude-fable-5-200k': 'claude-fable-5',
|
|
238
|
+
'fable': 'claude-fable-5[1m]',
|
|
239
|
+
'fable-200k': 'claude-fable-5',
|
|
240
|
+
'opus': 'claude-opus-4-8[1m]', // latest opus, 1M Max-included
|
|
226
241
|
'sonnet': 'claude-sonnet-4-6', // 200k default; use 'sonnet-1m' for explicit 1M
|
|
227
242
|
'sonnet-1m': 'claude-sonnet-4-6[1m]', // alias for 'sonnet' + explicit 1M opt-in
|
|
228
243
|
'haiku': 'claude-haiku-4-5-20251001',
|
|
@@ -298,6 +313,36 @@ function requireLocalOrigin(req, res, next) {
|
|
|
298
313
|
next();
|
|
299
314
|
}
|
|
300
315
|
|
|
316
|
+
function serializeSession(key, entry, { dashboard = false } = {}) {
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const idleMs = now - entry.lastUsed;
|
|
319
|
+
const ttlRemainingMs = Math.max(0, SESSION_TTL_MS - idleMs);
|
|
320
|
+
|
|
321
|
+
if (dashboard) {
|
|
322
|
+
return {
|
|
323
|
+
key,
|
|
324
|
+
sdkSessionId: entry.sdkSessionId,
|
|
325
|
+
model: entry.model,
|
|
326
|
+
messageCount: entry.messageCount,
|
|
327
|
+
createdAt: new Date(entry.createdAt).toISOString(),
|
|
328
|
+
lastUsedAt: new Date(entry.lastUsed).toISOString(),
|
|
329
|
+
idleSec: Math.floor(idleMs / 1000),
|
|
330
|
+
ttlRemainingSec: Math.floor(ttlRemainingMs / 1000),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
sessionKey: key,
|
|
336
|
+
sdkSessionId: entry.sdkSessionId,
|
|
337
|
+
model: entry.model,
|
|
338
|
+
messageCount: entry.messageCount,
|
|
339
|
+
createdAt: new Date(entry.createdAt).toISOString(),
|
|
340
|
+
lastUsed: new Date(entry.lastUsed).toISOString(),
|
|
341
|
+
idleSeconds: Math.round(idleMs / 1000),
|
|
342
|
+
ttlRemainingSeconds: Math.round(ttlRemainingMs / 1000),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
301
346
|
// GET / — serve dashboard. No-cache headers so browsers always re-fetch
|
|
302
347
|
// after a mobygate upgrade; otherwise they keep serving the old index.html
|
|
303
348
|
// from cache and users see a stale dashboard long after the service updated.
|
|
@@ -330,6 +375,19 @@ app.get('/', async (_req, res) => {
|
|
|
330
375
|
}
|
|
331
376
|
});
|
|
332
377
|
|
|
378
|
+
app.get('/dashboard.css', async (_req, res) => {
|
|
379
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
380
|
+
res.setHeader('Pragma', 'no-cache');
|
|
381
|
+
res.setHeader('Expires', '0');
|
|
382
|
+
try {
|
|
383
|
+
const { readFile } = await import('fs/promises');
|
|
384
|
+
const css = await readFile(join(__dirname, 'dashboard.css'), 'utf8');
|
|
385
|
+
res.type('css').send(css);
|
|
386
|
+
} catch (e) {
|
|
387
|
+
res.status(404).type('text').send('dashboard.css not found at ' + join(__dirname, 'dashboard.css'));
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
333
391
|
// /inspector — session inspector UI for browsing captures.
|
|
334
392
|
// Backed by /dashboard/captures and /dashboard/captures/:filename.
|
|
335
393
|
app.get('/inspector', async (_req, res) => {
|
|
@@ -619,37 +677,19 @@ app.get('/v1/models', (_req, res) => {
|
|
|
619
677
|
});
|
|
620
678
|
|
|
621
679
|
// GET /sessions — list active sessions
|
|
622
|
-
app.get('/sessions', (_req, res) => {
|
|
680
|
+
app.get('/sessions', requireLocalOrigin, (_req, res) => {
|
|
623
681
|
const list = [];
|
|
624
682
|
for (const [key, entry] of sessions) {
|
|
625
|
-
list.push(
|
|
626
|
-
sessionKey: key,
|
|
627
|
-
sdkSessionId: entry.sdkSessionId,
|
|
628
|
-
model: entry.model,
|
|
629
|
-
messageCount: entry.messageCount,
|
|
630
|
-
createdAt: new Date(entry.createdAt).toISOString(),
|
|
631
|
-
lastUsed: new Date(entry.lastUsed).toISOString(),
|
|
632
|
-
idleSeconds: Math.round((Date.now() - entry.lastUsed) / 1000),
|
|
633
|
-
ttlRemainingSeconds: Math.max(0, Math.round((SESSION_TTL_MS - (Date.now() - entry.lastUsed)) / 1000)),
|
|
634
|
-
});
|
|
683
|
+
list.push(serializeSession(key, entry));
|
|
635
684
|
}
|
|
636
685
|
res.json({ active: list.length, sessions: list });
|
|
637
686
|
});
|
|
638
687
|
|
|
639
688
|
// GET /sessions/:key — get specific session
|
|
640
|
-
app.get('/sessions/:key', (req, res) => {
|
|
689
|
+
app.get('/sessions/:key', requireLocalOrigin, (req, res) => {
|
|
641
690
|
const entry = sessions.get(req.params.key);
|
|
642
691
|
if (!entry) return res.status(404).json({ error: 'Session not found' });
|
|
643
|
-
res.json(
|
|
644
|
-
sessionKey: req.params.key,
|
|
645
|
-
sdkSessionId: entry.sdkSessionId,
|
|
646
|
-
model: entry.model,
|
|
647
|
-
messageCount: entry.messageCount,
|
|
648
|
-
createdAt: new Date(entry.createdAt).toISOString(),
|
|
649
|
-
lastUsed: new Date(entry.lastUsed).toISOString(),
|
|
650
|
-
idleSeconds: Math.round((Date.now() - entry.lastUsed) / 1000),
|
|
651
|
-
ttlRemainingSeconds: Math.max(0, Math.round((SESSION_TTL_MS - (Date.now() - entry.lastUsed)) / 1000)),
|
|
652
|
-
});
|
|
692
|
+
res.json(serializeSession(req.params.key, entry));
|
|
653
693
|
});
|
|
654
694
|
|
|
655
695
|
// DELETE /sessions/:key — clear a session
|
|
@@ -686,7 +726,7 @@ app.get('/health', (_req, res) => {
|
|
|
686
726
|
// GET /auth/status
|
|
687
727
|
// Reports CLI-side auth state plus (optionally) a real probe against Anthropic.
|
|
688
728
|
// Pass ?quick=1 to skip the probe (reads keychain only — cheap).
|
|
689
|
-
app.get('/auth/status', async (req, res) => {
|
|
729
|
+
app.get('/auth/status', requireLocalOrigin, async (req, res) => {
|
|
690
730
|
const quick = req.query.quick === '1' || req.query.quick === 'true';
|
|
691
731
|
const status = await getAuthStatus();
|
|
692
732
|
if (!quick && status.ok && status.loggedIn) {
|
|
@@ -759,7 +799,7 @@ async function loadBuildMeta() {
|
|
|
759
799
|
}
|
|
760
800
|
|
|
761
801
|
// GET /dashboard/recent — ring-buffer snapshot for initial page load
|
|
762
|
-
app.get('/dashboard/recent', async (req, res) => {
|
|
802
|
+
app.get('/dashboard/recent', requireLocalOrigin, async (req, res) => {
|
|
763
803
|
const limit = Math.min(500, parseInt(req.query.limit || '100', 10));
|
|
764
804
|
res.json({
|
|
765
805
|
recent: dashboardBus.getRecent({ limit }),
|
|
@@ -772,20 +812,10 @@ app.get('/dashboard/recent', async (req, res) => {
|
|
|
772
812
|
});
|
|
773
813
|
|
|
774
814
|
// GET /dashboard/sessions — active session detail for the dashboard
|
|
775
|
-
app.get('/dashboard/sessions', (_req, res) => {
|
|
776
|
-
const now = Date.now();
|
|
815
|
+
app.get('/dashboard/sessions', requireLocalOrigin, (_req, res) => {
|
|
777
816
|
const list = [];
|
|
778
817
|
for (const [key, entry] of sessions) {
|
|
779
|
-
list.push({
|
|
780
|
-
key,
|
|
781
|
-
sdkSessionId: entry.sdkSessionId,
|
|
782
|
-
model: entry.model,
|
|
783
|
-
messageCount: entry.messageCount,
|
|
784
|
-
createdAt: new Date(entry.createdAt).toISOString(),
|
|
785
|
-
lastUsedAt: new Date(entry.lastUsed).toISOString(),
|
|
786
|
-
idleSec: Math.floor((now - entry.lastUsed) / 1000),
|
|
787
|
-
ttlRemainingSec: Math.max(0, Math.floor((SESSION_TTL_MS - (now - entry.lastUsed)) / 1000)),
|
|
788
|
-
});
|
|
818
|
+
list.push(serializeSession(key, entry, { dashboard: true }));
|
|
789
819
|
}
|
|
790
820
|
// Most recently used first
|
|
791
821
|
list.sort((a, b) => a.idleSec - b.idleSec);
|
|
@@ -1139,7 +1169,7 @@ app.get('/dashboard/session-costs', requireLocalOrigin, async (_req, res) => {
|
|
|
1139
1169
|
// GET /update/check — is there a newer mobygate on npm?
|
|
1140
1170
|
// Response: { current, latest, updateAvailable, installMode, canApply, cached, error }
|
|
1141
1171
|
// Safe to poll: the npm registry call is cached for 15 min in-process.
|
|
1142
|
-
app.get('/update/check', async (req, res) => {
|
|
1172
|
+
app.get('/update/check', requireLocalOrigin, async (req, res) => {
|
|
1143
1173
|
try {
|
|
1144
1174
|
const force = req.query.force === '1' || req.query.force === 'true';
|
|
1145
1175
|
const info = await getUpdateCheck({ force });
|
|
@@ -1171,7 +1201,7 @@ app.post('/update/apply', requireLocalOrigin, (_req, res) => {
|
|
|
1171
1201
|
// The dashboard polls this during apply. `running` is determined by
|
|
1172
1202
|
// PID liveness, so even if our process is the one getting restarted,
|
|
1173
1203
|
// the new one answers correctly.
|
|
1174
|
-
app.get('/update/status', (req, res) => {
|
|
1204
|
+
app.get('/update/status', requireLocalOrigin, (req, res) => {
|
|
1175
1205
|
const state = readUpdateState();
|
|
1176
1206
|
let running = false;
|
|
1177
1207
|
if (state.pid) {
|