viveworker 0.7.0 → 0.8.1

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.
Files changed (54) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +115 -4
  3. package/package.json +13 -3
  4. package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
  5. package/plugins/viveworker-control-plane/.mcp.json +11 -0
  6. package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
  7. package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
  8. package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
  9. package/scripts/lib/markdown-render.mjs +128 -1
  10. package/scripts/lib/remote-pairing/README.md +164 -0
  11. package/scripts/lib/remote-pairing/_browser-bundle-entry.mjs +86 -0
  12. package/scripts/lib/remote-pairing/audit.mjs +122 -0
  13. package/scripts/lib/remote-pairing/bridge-relay-client.mjs +737 -0
  14. package/scripts/lib/remote-pairing/control.mjs +156 -0
  15. package/scripts/lib/remote-pairing/envelope.mjs +224 -0
  16. package/scripts/lib/remote-pairing/http-dispatch.mjs +530 -0
  17. package/scripts/lib/remote-pairing/keys-core.mjs +110 -0
  18. package/scripts/lib/remote-pairing/keys.mjs +181 -0
  19. package/scripts/lib/remote-pairing/noise.mjs +436 -0
  20. package/scripts/lib/remote-pairing/orchestrator.mjs +356 -0
  21. package/scripts/lib/remote-pairing/pairings.mjs +446 -0
  22. package/scripts/lib/remote-pairing/rpc.mjs +381 -0
  23. package/scripts/mcp-server.mjs +891 -0
  24. package/scripts/moltbook-scout-auto.sh +16 -8
  25. package/scripts/share-cli.mjs +14 -130
  26. package/scripts/stats-cli.mjs +683 -0
  27. package/scripts/viveworker-bridge.mjs +1676 -127
  28. package/scripts/viveworker.mjs +289 -7
  29. package/skills/viveworker-control-plane/SKILL.md +104 -0
  30. package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
  31. package/templates/CLAUDE.viveworker.md +67 -0
  32. package/web/app.css +683 -9
  33. package/web/app.js +1780 -187
  34. package/web/build-id.js +1 -0
  35. package/web/i18n.js +187 -9
  36. package/web/icons/apple-touch-icon.png +0 -0
  37. package/web/icons/viveworker-icon-192.png +0 -0
  38. package/web/icons/viveworker-icon-512.png +0 -0
  39. package/web/icons/viveworker-v-pulse.svg +16 -1
  40. package/web/index.html +32 -2
  41. package/web/remote-pairing/api-router.js +873 -0
  42. package/web/remote-pairing/keys.js +237 -0
  43. package/web/remote-pairing/pairing-state.js +313 -0
  44. package/web/remote-pairing/rpc-client.js +765 -0
  45. package/web/remote-pairing/transport.js +804 -0
  46. package/web/remote-pairing/wake.js +149 -0
  47. package/web/remote-pairing-test.html +400 -0
  48. package/web/remote-pairing.bundle.js +3 -0
  49. package/web/remote-pairing.bundle.js.LEGAL.txt +15 -0
  50. package/web/remote-pairing.bundle.js.map +7 -0
  51. package/web/sw.js +190 -20
  52. package/web/icons/viveworker-beacon-v.svg +0 -19
  53. package/web/icons/viveworker-icon-1024.png +0 -0
  54. package/web/icons/viveworker-v-check.svg +0 -19
package/web/sw.js CHANGED
@@ -1,9 +1,91 @@
1
- const CACHE_NAME = "viveworker-v66";
1
+ const APP_BUILD_ID = "__VIVEWORKER_APP_BUILD_ID__";
2
+ const CACHE_NAME = `viveworker-${APP_BUILD_ID}`;
3
+ const APP_SCRIPT_URL = `/app.js?v=${APP_BUILD_ID}`;
4
+ const API_ROUTER_URL = `/remote-pairing/api-router.js?v=${APP_BUILD_ID}`;
2
5
  const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
3
6
  const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
4
- const APP_ASSETS = ["/app.css", "/app.js", "/i18n.js", "/icons/viveworker-v-pulse.svg"];
7
+ // Cold off-LAN start requires the app shell plus remote-pairing modules in the
8
+ // precache. The first installed /app?pairToken=... navigation may happen before
9
+ // this service worker controls the page, so cache /app during install instead
10
+ // of relying on a later stale-while-revalidate pass.
11
+ //
12
+ // Bumping CACHE_NAME forces a re-cache so existing clients pick up the wider
13
+ // asset set on next launch.
14
+ const APP_ASSETS = [
15
+ "/app",
16
+ APP_SCRIPT_URL,
17
+ "/app.css",
18
+ "/app.js",
19
+ "/i18n.js",
20
+ "/icons/viveworker-v-pulse.svg",
21
+ API_ROUTER_URL,
22
+ "/remote-pairing/api-router.js",
23
+ "/remote-pairing/keys.js",
24
+ "/remote-pairing/pairing-state.js",
25
+ "/remote-pairing/rpc-client.js",
26
+ "/remote-pairing/transport.js",
27
+ "/remote-pairing/wake.js",
28
+ "/remote-pairing.bundle.js",
29
+ ];
5
30
  const APP_ROUTES = new Set(["/", "/app", "/app/"]);
6
- const CACHED_PATHS = new Set(APP_ASSETS);
31
+ const CACHED_PATHS = new Set(APP_ASSETS.map((asset) => new URL(asset, self.location.origin).pathname));
32
+ const VERSIONED_CACHE_PATHS = new Set([
33
+ "/app.js",
34
+ "/remote-pairing/api-router.js",
35
+ ]);
36
+ const NETWORK_FIRST_PATHS = new Set([
37
+ "/app.js",
38
+ "/remote-pairing/api-router.js",
39
+ ]);
40
+ const APP_NAVIGATION_NETWORK_TIMEOUT_MS = 1800;
41
+ const ASSET_NETWORK_TIMEOUT_MS = 900;
42
+ const APP_SHELL_FALLBACK_HTML = `<!doctype html>
43
+ <html lang="en">
44
+ <head>
45
+ <meta charset="utf-8">
46
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
47
+ <meta name="theme-color" content="#101418">
48
+ <meta name="apple-mobile-web-app-capable" content="yes">
49
+ <link rel="manifest" href="/manifest.webmanifest">
50
+ <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
51
+ <link rel="icon" type="image/png" sizes="192x192" href="/icons/viveworker-icon-192.png">
52
+ <link rel="stylesheet" href="/app.css">
53
+ <style>
54
+ html, body { min-height: 100%; margin: 0; background: #081015; color: #f5fbff; }
55
+ .boot-splash { position: fixed; inset: 0; display: grid; place-items: center; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; background: radial-gradient(circle at 50% 18%, rgba(47, 143, 103, 0.22), transparent 30%), linear-gradient(180deg, #081015 0%, #091015 100%); }
56
+ .boot-splash__card { display: grid; justify-items: center; gap: 0.9rem; text-align: center; }
57
+ .boot-splash__logo { width: 6rem; height: 6rem; border-radius: 28%; }
58
+ .boot-splash__title { margin: 0; font-size: 2rem; letter-spacing: -0.04em; }
59
+ .boot-splash__status { min-height: 1.25em; margin: 0; color: rgba(205, 220, 231, 0.72); }
60
+ .boot-splash__hint { max-width: 15rem; margin: -0.35rem 0 0; color: rgba(178, 196, 210, 0.58); font-size: 0.78rem; line-height: 1.45; opacity: 0; transition: opacity 220ms ease; }
61
+ .boot-splash__hint.is-visible { opacity: 1; }
62
+ .viveworker-ready .boot-splash { opacity: 0; visibility: hidden; }
63
+ </style>
64
+ <title>viveworker</title>
65
+ </head>
66
+ <body>
67
+ <div id="boot-splash" class="boot-splash" role="status" aria-live="polite" aria-label="viveworker is starting">
68
+ <div class="boot-splash__card">
69
+ <img class="boot-splash__logo" src="/icons/viveworker-v-pulse.svg" alt="" width="112" height="112" decoding="async">
70
+ <h1 class="boot-splash__title">viveworker</h1>
71
+ <p id="boot-splash-status" class="boot-splash__status">Checking your trusted Wi-Fi...</p>
72
+ <p id="boot-splash-hint" class="boot-splash__hint" hidden>The first remote connection can take tens of seconds.</p>
73
+ </div>
74
+ </div>
75
+ <div id="app"></div>
76
+ <script>
77
+ (() => {
78
+ const isJa = (navigator.language || "").toLowerCase().startsWith("ja");
79
+ const message = isJa ? "同じWi-Fi内のPCを確認中..." : "Checking your trusted Wi-Fi...";
80
+ const status = document.getElementById("boot-splash-status");
81
+ const splash = document.getElementById("boot-splash");
82
+ if (status) status.textContent = message;
83
+ if (splash) splash.setAttribute("aria-label", "viveworker " + message);
84
+ })();
85
+ </script>
86
+ <script type="module" src="${APP_SCRIPT_URL}"></script>
87
+ </body>
88
+ </html>`;
7
89
 
8
90
  self.addEventListener("install", (event) => {
9
91
  event.waitUntil(
@@ -44,12 +126,20 @@ self.addEventListener("fetch", (event) => {
44
126
  }
45
127
 
46
128
  if (APP_ROUTES.has(url.pathname)) {
47
- event.respondWith(staleWhileRevalidate(event, "/app"));
129
+ event.respondWith(networkFirstWithFallback(event, "/app", {
130
+ timeoutMs: APP_NAVIGATION_NETWORK_TIMEOUT_MS,
131
+ fallbackAppShell: true,
132
+ }));
48
133
  return;
49
134
  }
50
135
 
51
136
  if (CACHED_PATHS.has(url.pathname)) {
52
- event.respondWith(staleWhileRevalidate(event, url.pathname));
137
+ const cacheKey = cacheKeyForUrl(url);
138
+ event.respondWith(
139
+ NETWORK_FIRST_PATHS.has(url.pathname)
140
+ ? networkFirstWithFallback(event, cacheKey, { timeoutMs: ASSET_NETWORK_TIMEOUT_MS })
141
+ : staleWhileRevalidate(event, cacheKey)
142
+ );
53
143
  }
54
144
  });
55
145
 
@@ -72,7 +162,17 @@ self.addEventListener("push", (event) => {
72
162
  data: payload.data || { url: "/app" },
73
163
  };
74
164
 
75
- event.waitUntil(self.registration.showNotification(title, options));
165
+ event.waitUntil((async () => {
166
+ await self.registration.showNotification(title, options);
167
+ // Phase 2d: if the push is tagged as a remote-pairing wake hint, also
168
+ // broadcast a postMessage to open clients so any active transport
169
+ // instance can kick its reconnect immediately. Background-only pushes
170
+ // (no PWA window open) hit the notification path; the user's tap then
171
+ // foregrounds the PWA which kicks the transport via visibilitychange.
172
+ if (shouldBroadcastRemotePairingWake(payload)) {
173
+ await notifyClients("remote-pairing-wake", { reason: "push" });
174
+ }
175
+ })());
76
176
  });
77
177
 
78
178
  self.addEventListener("notificationclick", (event) => {
@@ -172,29 +272,38 @@ async function persistNotificationIntent(url) {
172
272
  }
173
273
  }
174
274
 
175
- async function notifyClients(type) {
275
+ async function notifyClients(type, extra) {
176
276
  const clients = await self.clients.matchAll({
177
277
  type: "window",
178
278
  includeUncontrolled: true,
179
279
  });
280
+ // Spread `extra` after `type` so callers can't accidentally clobber the
281
+ // discriminator the receiving client uses to dispatch.
282
+ const message = extra ? { ...extra, type } : { type };
180
283
  for (const client of clients) {
181
- client.postMessage({ type });
284
+ client.postMessage(message);
182
285
  }
183
286
  }
184
287
 
185
- // Cache-first with background revalidation. Flips the previous networkFirst
186
- // strategy: instead of blocking first paint on a fresh fetch of the ~450KB
187
- // app shell (HTML + app.js + app.css + i18n.js) every launch, we serve the
188
- // cached copy immediately and refresh it in the background for the next
189
- // visit. Updates still land quickly because the SW itself is served fresh
190
- // from the bridge (`/sw.js` is excluded from this handler above), and a new
191
- // SW's `install` event pre-populates the cache with the latest assets
192
- // before `activate`/`clients.claim()` triggers a reload in page script.
193
- async function staleWhileRevalidate(event, cacheKey) {
194
- const cache = await caches.open(CACHE_NAME);
195
- const cached = await cache.match(cacheKey);
288
+ // Decide whether a Web Push payload is a remote-pairing wake hint. Both an
289
+ // explicit `data.kind === "remote-pairing-wake"` and a generic
290
+ // `data.kind === "remote-pairing-event"` count the latter lets the bridge
291
+ // piggy-back on existing pushes (approval requests, etc.) without sending a
292
+ // dedicated wake-only notification.
293
+ function shouldBroadcastRemotePairingWake(payload) {
294
+ const kind = payload?.data?.kind;
295
+ return kind === "remote-pairing-wake" || kind === "remote-pairing-event";
296
+ }
297
+
298
+ function cacheKeyForUrl(url) {
299
+ if (VERSIONED_CACHE_PATHS.has(url.pathname) && url.search) {
300
+ return `${url.pathname}${url.search}`;
301
+ }
302
+ return url.pathname;
303
+ }
196
304
 
197
- const networkPromise = fetch(event.request, { cache: "no-store" })
305
+ function fetchAndCache(event, cache, cacheKey) {
306
+ return fetch(event.request, { cache: "no-store" })
198
307
  .then(async (response) => {
199
308
  if (response && response.ok) {
200
309
  await cache.put(cacheKey, response.clone());
@@ -202,6 +311,58 @@ async function staleWhileRevalidate(event, cacheKey) {
202
311
  return response;
203
312
  })
204
313
  .catch(() => null);
314
+ }
315
+
316
+ async function networkFirstWithFallback(event, cacheKey, options = {}) {
317
+ const cache = await caches.open(CACHE_NAME);
318
+ const cachedPromise = cache.match(cacheKey);
319
+ const networkPromise = fetchAndCache(event, cache, cacheKey);
320
+ const timeoutMs = Math.max(0, Number(options.timeoutMs ?? 0) || 0);
321
+
322
+ let response = null;
323
+ if (timeoutMs > 0) {
324
+ response = await Promise.race([
325
+ networkPromise,
326
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs)),
327
+ ]);
328
+ } else {
329
+ response = await networkPromise;
330
+ }
331
+
332
+ if (response) {
333
+ return response;
334
+ }
335
+
336
+ // If the network attempt is still pending after the timeout, keep the
337
+ // Service Worker alive so LAN updates are cached for the next launch.
338
+ event.waitUntil(networkPromise);
339
+
340
+ const cached = await cachedPromise;
341
+ if (cached) {
342
+ return cached;
343
+ }
344
+
345
+ if (options.fallbackAppShell) {
346
+ return new Response(APP_SHELL_FALLBACK_HTML, {
347
+ status: 200,
348
+ headers: {
349
+ "Content-Type": "text/html; charset=utf-8",
350
+ "Cache-Control": "no-store",
351
+ },
352
+ });
353
+ }
354
+
355
+ const lateResponse = await networkPromise;
356
+ return lateResponse || Response.error();
357
+ }
358
+
359
+ // Cache-first with background revalidation for non-critical assets. App entry
360
+ // points stay network-first so LAN refreshes can replace stale PWA code before
361
+ // the device goes back to relay-only mode.
362
+ async function staleWhileRevalidate(event, cacheKey) {
363
+ const cache = await caches.open(CACHE_NAME);
364
+ const cached = await cache.match(cacheKey);
365
+ const networkPromise = fetchAndCache(event, cache, cacheKey);
205
366
 
206
367
  if (cached) {
207
368
  // Keep the SW alive long enough to persist the background refresh even
@@ -214,5 +375,14 @@ async function staleWhileRevalidate(event, cacheKey) {
214
375
  if (response) {
215
376
  return response;
216
377
  }
378
+ if (cacheKey === "/app") {
379
+ return new Response(APP_SHELL_FALLBACK_HTML, {
380
+ status: 200,
381
+ headers: {
382
+ "Content-Type": "text/html; charset=utf-8",
383
+ "Cache-Control": "no-store",
384
+ },
385
+ });
386
+ }
217
387
  return Response.error();
218
388
  }
@@ -1,19 +0,0 @@
1
- <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <defs>
3
- <radialGradient id="bg" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(524 120) rotate(90) scale(920 834)">
4
- <stop stop-color="#132432"/>
5
- <stop offset="0.45" stop-color="#0C151C"/>
6
- <stop offset="1" stop-color="#091016"/>
7
- </radialGradient>
8
- <radialGradient id="signal" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(512 244) rotate(90) scale(182 244)">
9
- <stop stop-color="#84CBFF" stop-opacity="0.3"/>
10
- <stop offset="1" stop-color="#84CBFF" stop-opacity="0"/>
11
- </radialGradient>
12
- </defs>
13
- <rect width="1024" height="1024" rx="224" fill="url(#bg)"/>
14
- <ellipse cx="512" cy="246" rx="184" ry="132" fill="url(#signal)"/>
15
- <path d="M322 370L477 718C493 754 531 754 547 718L702 370" stroke="#F4FAFF" stroke-width="96" stroke-linecap="round" stroke-linejoin="round"/>
16
- <circle cx="512" cy="228" r="28" fill="#8CCBFF"/>
17
- <path d="M438 216C438 175 471 142 512 142C553 142 586 175 586 216" stroke="#8CCBFF" stroke-width="30" stroke-linecap="round"/>
18
- <path d="M384 220C384 149 441 92 512 92C583 92 640 149 640 220" stroke="#8CCBFF" stroke-width="20" stroke-linecap="round" opacity="0.95"/>
19
- </svg>
Binary file
@@ -1,19 +0,0 @@
1
- <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
2
- <defs>
3
- <linearGradient id="bg" x1="188" y1="122" x2="841" y2="931" gradientUnits="userSpaceOnUse">
4
- <stop stop-color="#16222B"/>
5
- <stop offset="0.48" stop-color="#0D141A"/>
6
- <stop offset="1" stop-color="#0A1116"/>
7
- </linearGradient>
8
- <radialGradient id="accent" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(780 252) rotate(128.783) scale(251.649)">
9
- <stop stop-color="#8BE9C1" stop-opacity="0.34"/>
10
- <stop offset="1" stop-color="#8BE9C1" stop-opacity="0"/>
11
- </radialGradient>
12
- </defs>
13
- <rect width="1024" height="1024" rx="224" fill="url(#bg)"/>
14
- <circle cx="781" cy="254" r="120" fill="url(#accent)"/>
15
- <path d="M286 300L468 730C486 772 538 772 556 730L738 300" stroke="#F8FBFE" stroke-width="108" stroke-linecap="round" stroke-linejoin="round"/>
16
- <circle cx="760" cy="276" r="116" fill="#8BE9C1"/>
17
- <circle cx="760" cy="276" r="94" fill="#132028" fill-opacity="0.2"/>
18
- <path d="M707 277L741 312L815 238" stroke="#0C161C" stroke-width="42" stroke-linecap="round" stroke-linejoin="round"/>
19
- </svg>