@cordfuse/llmux 0.13.7 → 0.14.0

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 CHANGED
@@ -46,10 +46,11 @@ when a token expires — phone in, click through, phone out.
46
46
  That's the same surface you get for everyday driving: pick an agent on
47
47
  your phone over LTE, type a prompt into a real xterm with a soft-keyboard
48
48
  toolbar (Esc / Tab / Ctrl / arrows / shell chars), watch tool calls
49
- stream in. No "mobile app"it's the same daemon serving a real
50
- terminal over a WebSocket.
49
+ stream in. **The picker is a PWA** "Add to Home Screen" in Chrome or
50
+ Safari and it launches standalone (no browser chrome, splash screen, OS
51
+ task-switcher entry). Same daemon, same WebSocket, just feels native.
51
52
 
52
- > **Status:** v0.13.7 — daemon + CLI client consolidated into one binary
53
+ > **Status:** v0.14.0 — daemon + CLI client consolidated into one binary
53
54
  > (`llmux`). Auth, tokens, mobile picker, conversation resume, Claude Code
54
55
  > history adapter shipped. See [CHANGELOG.md](./CHANGELOG.md).
55
56
 
package/dist/index.js CHANGED
@@ -574,6 +574,68 @@ function listSessionViews() {
574
574
  }
575
575
  var FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0b0c10"/><rect x="5" y="5" width="9" height="9" fill="#7cc4ff"/><rect x="18" y="5" width="9" height="9" fill="#7cc4ff"/><rect x="5" y="18" width="9" height="9" fill="#7cc4ff"/><rect x="18" y="18" width="9" height="9" fill="#7cc4ff"/></svg>`;
576
576
  var FAVICON_DATA_URL = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}`;
577
+ var PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192"><rect width="192" height="192" fill="#0b0c10"/><rect x="30" y="30" width="58" height="58" rx="6" fill="#7cc4ff"/><rect x="104" y="30" width="58" height="58" rx="6" fill="#7cc4ff"/><rect x="30" y="104" width="58" height="58" rx="6" fill="#7cc4ff"/><rect x="104" y="104" width="58" height="58" rx="6" fill="#7cc4ff"/></svg>`;
578
+ var PWA_MANIFEST = JSON.stringify({
579
+ name: "llmux",
580
+ short_name: "llmux",
581
+ description: "tmux-based AI agent dispatcher \u2014 drive every CLI from your phone",
582
+ start_url: "/",
583
+ scope: "/",
584
+ display: "standalone",
585
+ orientation: "any",
586
+ background_color: "#0b0c10",
587
+ theme_color: "#0b0c10",
588
+ icons: [
589
+ { src: "/icon-192.svg", sizes: "192x192", type: "image/svg+xml", purpose: "any" },
590
+ { src: "/icon-512.svg", sizes: "512x512", type: "image/svg+xml", purpose: "any maskable" }
591
+ ]
592
+ });
593
+ var SW_CACHE_VERSION = "llmux-v1";
594
+ var PWA_SW_JS = `// llmux service worker (auto-registered from the picker page)
595
+ const CACHE = '${SW_CACHE_VERSION}';
596
+ const SHELL = ['/', '/manifest.webmanifest'];
597
+
598
+ self.addEventListener('install', (e) => {
599
+ e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL).catch(() => {})));
600
+ self.skipWaiting();
601
+ });
602
+
603
+ self.addEventListener('activate', (e) => {
604
+ e.waitUntil(
605
+ caches.keys().then((keys) =>
606
+ Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
607
+ ),
608
+ );
609
+ self.clients.claim();
610
+ });
611
+
612
+ self.addEventListener('fetch', (e) => {
613
+ const url = new URL(e.request.url);
614
+ // Skip non-GET, WebSocket, and same-origin API/WS paths \u2014 they need fresh
615
+ // server state and the SW must not interpose.
616
+ if (e.request.method !== 'GET') return;
617
+ if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws/')) return;
618
+ e.respondWith(
619
+ fetch(e.request)
620
+ .then((r) => {
621
+ if (r.ok && (url.pathname === '/' || url.pathname === '/manifest.webmanifest')) {
622
+ const copy = r.clone();
623
+ caches.open(CACHE).then((c) => c.put(e.request, copy)).catch(() => {});
624
+ }
625
+ return r;
626
+ })
627
+ .catch(() => caches.match(e.request).then((m) => m || new Response('offline', { status: 503 }))),
628
+ );
629
+ });
630
+ `;
631
+ var PWA_HEAD_TAGS = `<link rel="manifest" href="/manifest.webmanifest">
632
+ <meta name="theme-color" content="#0b0c10">
633
+ <meta name="apple-mobile-web-app-capable" content="yes">
634
+ <meta name="mobile-web-app-capable" content="yes">
635
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
636
+ <meta name="apple-mobile-web-app-title" content="llmux">
637
+ <meta name="application-name" content="llmux">
638
+ <script>if('serviceWorker' in navigator){window.addEventListener('load',function(){navigator.serviceWorker.register('/sw.js').catch(function(){})})}</script>`;
577
639
  function pickerPage() {
578
640
  const sessions = listSessionViews();
579
641
  return `<!doctype html><html lang="en"><head>
@@ -581,6 +643,7 @@ function pickerPage() {
581
643
  <title>LLMUX: Sessions</title>
582
644
  <link rel="icon" href="${FAVICON_DATA_URL}">
583
645
  <link rel="apple-touch-icon" href="${FAVICON_DATA_URL}">
646
+ ${PWA_HEAD_TAGS}
584
647
  <style>
585
648
  :root{color-scheme:dark}
586
649
  html,body{margin:0;background:#0b0c10;color:#e6e8eb;font-family:ui-monospace,monospace;font-size:14px;overflow-x:hidden}
@@ -2325,6 +2388,28 @@ function startServer(opts) {
2325
2388
  if (url.pathname === "/api/version" && method === "GET") {
2326
2389
  return sendJson(res, { version: DAEMON_VERSION });
2327
2390
  }
2391
+ if (url.pathname === "/manifest.webmanifest" && method === "GET") {
2392
+ res.writeHead(200, {
2393
+ "content-type": "application/manifest+json; charset=utf-8",
2394
+ "cache-control": "public, max-age=3600"
2395
+ });
2396
+ return res.end(PWA_MANIFEST);
2397
+ }
2398
+ if (url.pathname === "/sw.js" && method === "GET") {
2399
+ res.writeHead(200, {
2400
+ "content-type": "application/javascript; charset=utf-8",
2401
+ "cache-control": "no-cache",
2402
+ "service-worker-allowed": "/"
2403
+ });
2404
+ return res.end(PWA_SW_JS);
2405
+ }
2406
+ if ((url.pathname === "/icon-192.svg" || url.pathname === "/icon-512.svg") && method === "GET") {
2407
+ res.writeHead(200, {
2408
+ "content-type": "image/svg+xml; charset=utf-8",
2409
+ "cache-control": "public, max-age=86400"
2410
+ });
2411
+ return res.end(PWA_ICON_SVG);
2412
+ }
2328
2413
  if (url.pathname === "/api/auth" && method === "POST") {
2329
2414
  try {
2330
2415
  const body = await readJsonBody(req);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cordfuse/llmux",
3
- "version": "0.13.7",
3
+ "version": "0.14.0",
4
4
  "description": "tmux-based AI agent dispatcher — REST/WS daemon + CLI client in one binary",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -97,6 +97,81 @@ function listSessionViews(): SessionView[] {
97
97
  const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#0b0c10"/><rect x="5" y="5" width="9" height="9" fill="#7cc4ff"/><rect x="18" y="5" width="9" height="9" fill="#7cc4ff"/><rect x="5" y="18" width="9" height="9" fill="#7cc4ff"/><rect x="18" y="18" width="9" height="9" fill="#7cc4ff"/></svg>`;
98
98
  const FAVICON_DATA_URL = `data:image/svg+xml,${encodeURIComponent(FAVICON_SVG)}`;
99
99
 
100
+ // ---------- PWA assets ----------
101
+ // Manifest + service worker + icon SVGs served from always-open routes so
102
+ // the browser can discover them before the auth gate. The SW caches the
103
+ // app shell once the user is authed (the SW's fetch carries cookies).
104
+
105
+ const PWA_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 192"><rect width="192" height="192" fill="#0b0c10"/><rect x="30" y="30" width="58" height="58" rx="6" fill="#7cc4ff"/><rect x="104" y="30" width="58" height="58" rx="6" fill="#7cc4ff"/><rect x="30" y="104" width="58" height="58" rx="6" fill="#7cc4ff"/><rect x="104" y="104" width="58" height="58" rx="6" fill="#7cc4ff"/></svg>`;
106
+
107
+ const PWA_MANIFEST = JSON.stringify({
108
+ name: 'llmux',
109
+ short_name: 'llmux',
110
+ description: 'tmux-based AI agent dispatcher — drive every CLI from your phone',
111
+ start_url: '/',
112
+ scope: '/',
113
+ display: 'standalone',
114
+ orientation: 'any',
115
+ background_color: '#0b0c10',
116
+ theme_color: '#0b0c10',
117
+ icons: [
118
+ { src: '/icon-192.svg', sizes: '192x192', type: 'image/svg+xml', purpose: 'any' },
119
+ { src: '/icon-512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
120
+ ],
121
+ });
122
+
123
+ // Minimal service worker — network-first (this is a live dashboard, not a
124
+ // static site), cache fallback for the shell so the installed app opens
125
+ // even on flaky transit. Bumping CACHE_VERSION on schema-affecting changes
126
+ // will purge older caches on next activate.
127
+ const SW_CACHE_VERSION = 'llmux-v1';
128
+ const PWA_SW_JS = `// llmux service worker (auto-registered from the picker page)
129
+ const CACHE = '${SW_CACHE_VERSION}';
130
+ const SHELL = ['/', '/manifest.webmanifest'];
131
+
132
+ self.addEventListener('install', (e) => {
133
+ e.waitUntil(caches.open(CACHE).then((c) => c.addAll(SHELL).catch(() => {})));
134
+ self.skipWaiting();
135
+ });
136
+
137
+ self.addEventListener('activate', (e) => {
138
+ e.waitUntil(
139
+ caches.keys().then((keys) =>
140
+ Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
141
+ ),
142
+ );
143
+ self.clients.claim();
144
+ });
145
+
146
+ self.addEventListener('fetch', (e) => {
147
+ const url = new URL(e.request.url);
148
+ // Skip non-GET, WebSocket, and same-origin API/WS paths — they need fresh
149
+ // server state and the SW must not interpose.
150
+ if (e.request.method !== 'GET') return;
151
+ if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws/')) return;
152
+ e.respondWith(
153
+ fetch(e.request)
154
+ .then((r) => {
155
+ if (r.ok && (url.pathname === '/' || url.pathname === '/manifest.webmanifest')) {
156
+ const copy = r.clone();
157
+ caches.open(CACHE).then((c) => c.put(e.request, copy)).catch(() => {});
158
+ }
159
+ return r;
160
+ })
161
+ .catch(() => caches.match(e.request).then((m) => m || new Response('offline', { status: 503 }))),
162
+ );
163
+ });
164
+ `;
165
+
166
+ const PWA_HEAD_TAGS = `<link rel="manifest" href="/manifest.webmanifest">
167
+ <meta name="theme-color" content="#0b0c10">
168
+ <meta name="apple-mobile-web-app-capable" content="yes">
169
+ <meta name="mobile-web-app-capable" content="yes">
170
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
171
+ <meta name="apple-mobile-web-app-title" content="llmux">
172
+ <meta name="application-name" content="llmux">
173
+ <script>if('serviceWorker' in navigator){window.addEventListener('load',function(){navigator.serviceWorker.register('/sw.js').catch(function(){})})}</script>`;
174
+
100
175
  // ---------- pages ----------
101
176
 
102
177
  function pickerPage(): string {
@@ -106,6 +181,7 @@ function pickerPage(): string {
106
181
  <title>LLMUX: Sessions</title>
107
182
  <link rel="icon" href="${FAVICON_DATA_URL}">
108
183
  <link rel="apple-touch-icon" href="${FAVICON_DATA_URL}">
184
+ ${PWA_HEAD_TAGS}
109
185
  <style>
110
186
  :root{color-scheme:dark}
111
187
  html,body{margin:0;background:#0b0c10;color:#e6e8eb;font-family:ui-monospace,monospace;font-size:14px;overflow-x:hidden}
@@ -1983,6 +2059,30 @@ export function startServer(opts: ServeOptions): ServerHandle {
1983
2059
  if (url.pathname === '/api/version' && method === 'GET') {
1984
2060
  return sendJson(res, { version: DAEMON_VERSION });
1985
2061
  }
2062
+ // PWA discovery — manifest, service worker, icons. All must be reachable
2063
+ // before the auth gate so the browser can install the app.
2064
+ if (url.pathname === '/manifest.webmanifest' && method === 'GET') {
2065
+ res.writeHead(200, {
2066
+ 'content-type': 'application/manifest+json; charset=utf-8',
2067
+ 'cache-control': 'public, max-age=3600',
2068
+ });
2069
+ return res.end(PWA_MANIFEST);
2070
+ }
2071
+ if (url.pathname === '/sw.js' && method === 'GET') {
2072
+ res.writeHead(200, {
2073
+ 'content-type': 'application/javascript; charset=utf-8',
2074
+ 'cache-control': 'no-cache',
2075
+ 'service-worker-allowed': '/',
2076
+ });
2077
+ return res.end(PWA_SW_JS);
2078
+ }
2079
+ if ((url.pathname === '/icon-192.svg' || url.pathname === '/icon-512.svg') && method === 'GET') {
2080
+ res.writeHead(200, {
2081
+ 'content-type': 'image/svg+xml; charset=utf-8',
2082
+ 'cache-control': 'public, max-age=86400',
2083
+ });
2084
+ return res.end(PWA_ICON_SVG);
2085
+ }
1986
2086
 
1987
2087
  // ---- Auth gate (POST /api/auth, no prior auth required) ----
1988
2088
  if (url.pathname === '/api/auth' && method === 'POST') {