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 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
- <script src="https://cdn.tailwindcss.com"></script>
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.2",
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.2.112",
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-7[1m]';
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
- 'claude-opus-4': 'claude-opus-4-7[1m]',
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
- 'opus': 'claude-opus-4-7[1m]', // Opus 1M is Max-included
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) {