@cordfuse/llmux 0.13.6 → 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 +11 -3
- package/dist/index.js +85 -0
- package/package.json +1 -1
- package/src/daemon/web/server.ts +100 -0
package/README.md
CHANGED
|
@@ -27,6 +27,13 @@ The agent runs unmodified and doesn't know it's being driven headlessly.
|
|
|
27
27
|
Tool state persists across prompts. Conversations are resumable from any
|
|
28
28
|
client.
|
|
29
29
|
|
|
30
|
+
That same surface is the substrate higher-level orchestration sits on top
|
|
31
|
+
of — spec-driven development (SDD) pipelines, multi-agent chains,
|
|
32
|
+
scheduled jobs, evals harnessed against live agents — all just `llmux
|
|
33
|
+
session prompt <name> "..."` calls. No special agent build, no headless
|
|
34
|
+
SDK, no mode flag. If a human can drive the agent, llmux can drive it
|
|
35
|
+
the same way.
|
|
36
|
+
|
|
30
37
|
### OAuth from your phone, on a headless box
|
|
31
38
|
|
|
32
39
|
A consequence of driving real interactive agents: **OAuth works even when
|
|
@@ -39,10 +46,11 @@ when a token expires — phone in, click through, phone out.
|
|
|
39
46
|
That's the same surface you get for everyday driving: pick an agent on
|
|
40
47
|
your phone over LTE, type a prompt into a real xterm with a soft-keyboard
|
|
41
48
|
toolbar (Esc / Tab / Ctrl / arrows / shell chars), watch tool calls
|
|
42
|
-
stream in.
|
|
43
|
-
|
|
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.
|
|
44
52
|
|
|
45
|
-
> **Status:** v0.
|
|
53
|
+
> **Status:** v0.14.0 — daemon + CLI client consolidated into one binary
|
|
46
54
|
> (`llmux`). Auth, tokens, mobile picker, conversation resume, Claude Code
|
|
47
55
|
> history adapter shipped. See [CHANGELOG.md](./CHANGELOG.md).
|
|
48
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
package/src/daemon/web/server.ts
CHANGED
|
@@ -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') {
|