bloby-bot 0.69.4 → 0.69.6
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/package.json +1 -1
- package/supervisor/channels/whatsapp.ts +25 -22
- package/supervisor/index.ts +224 -20
- package/supervisor/public/morphy/headphones-idle.webp +0 -0
- package/supervisor/public/morphy/headphones.json +4 -4
- package/supervisor/public/morphy/headphones.webp +0 -0
- package/supervisor/public/morphy/teleporting.json +2 -2
- package/supervisor/public/morphy/teleporting.webp +0 -0
- package/supervisor/shell.ts +53 -0
- package/supervisor/vite-dev.ts +15 -1
- package/supervisor/widget.js +124 -24
- package/supervisor/workspace-guard.js +33 -0
- package/vite.config.ts +22 -0
- package/workspace/client/public/morphy/headphones-idle.webp +0 -0
- package/workspace/client/public/morphy/headphones.json +4 -4
- package/workspace/client/public/morphy/headphones.webp +0 -0
- package/workspace/client/public/morphy/teleporting.json +2 -2
- package/workspace/client/public/morphy/teleporting.webp +0 -0
- package/workspace/client/public/sw.js +25 -2
- package/workspace/skills/create-skill/SKILL.md +188 -0
- package/workspace/skills/create-skill/references/patterns.md +126 -0
- package/workspace/client/public/arrow.png +0 -0
- package/workspace/client/public/bloby_happy.mov +0 -0
- package/workspace/client/public/bloby_happy.webm +0 -0
- package/workspace/client/public/bloby_happy_reappearing.mov +0 -0
- package/workspace/client/public/bloby_happy_reappearing.webm +0 -0
- package/workspace/client/public/bloby_say_hi.mov +0 -0
- package/workspace/client/public/bloby_say_hi.webm +0 -0
- package/workspace/client/public/bloby_tilts.webm +0 -0
- package/workspace/client/public/headphones_spritesheet.webp +0 -0
- package/workspace/client/public/spritesheet.webp +0 -0
package/supervisor/widget.js
CHANGED
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
try { if (window.top !== window) return; } catch (e) { return; }
|
|
17
17
|
if (document.getElementById('bloby-widget')) return;
|
|
18
18
|
|
|
19
|
+
// Perf marks land in the shell's beacon (see shell.ts) — no-op when the collector is absent.
|
|
20
|
+
function pmark(name) { try { if (window.__blobyPerf) window.__blobyPerf.mark(name); } catch (e) {} }
|
|
21
|
+
pmark('widget:start');
|
|
22
|
+
|
|
19
23
|
var PANEL_WIDTH = '480px';
|
|
20
24
|
|
|
21
25
|
// ── Styles ──
|
|
@@ -42,9 +46,50 @@
|
|
|
42
46
|
panel.id = 'bloby-widget-panel';
|
|
43
47
|
document.body.appendChild(panel);
|
|
44
48
|
|
|
49
|
+
// The chat app is ~1 MB of JS — loading it at parse time competed head-to-head with the
|
|
50
|
+
// workspace module waterfall on the same (often residential) uplink while the panel was
|
|
51
|
+
// closed. The iframe element exists from the start (layout, contentWindow targets) but gets
|
|
52
|
+
// its src lazily: on first open, when the workspace signals ready (+idle), or a hard 8s
|
|
53
|
+
// backstop — the chat is the user's lifeline and must always self-load eventually.
|
|
54
|
+
// Messages destined for the chat queue until its document is actually loaded.
|
|
45
55
|
var iframe = document.createElement('iframe');
|
|
46
|
-
iframe.src = '/bloby/';
|
|
47
56
|
panel.appendChild(iframe);
|
|
57
|
+
var chatRequested = false;
|
|
58
|
+
var chatReady = false;
|
|
59
|
+
var chatQueue = [];
|
|
60
|
+
|
|
61
|
+
function ensureChatLoaded() {
|
|
62
|
+
if (chatRequested) return;
|
|
63
|
+
chatRequested = true;
|
|
64
|
+
pmark('widget:chat-requested');
|
|
65
|
+
iframe.addEventListener('load', function () {
|
|
66
|
+
chatReady = true;
|
|
67
|
+
pmark('widget:chat-loaded');
|
|
68
|
+
for (var i = 0; i < chatQueue.length; i++) {
|
|
69
|
+
try { iframe.contentWindow.postMessage(chatQueue[i], location.origin); } catch (e) {}
|
|
70
|
+
}
|
|
71
|
+
chatQueue = [];
|
|
72
|
+
});
|
|
73
|
+
iframe.src = '/bloby/';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function postToChat(msg) {
|
|
77
|
+
if (chatReady && iframe.contentWindow) {
|
|
78
|
+
iframe.contentWindow.postMessage(msg, location.origin);
|
|
79
|
+
} else {
|
|
80
|
+
chatQueue.push(msg);
|
|
81
|
+
ensureChatLoaded();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function scheduleChatLoad() {
|
|
86
|
+
if (chatRequested) return;
|
|
87
|
+
if (typeof requestIdleCallback === 'function') requestIdleCallback(ensureChatLoaded, { timeout: 4000 });
|
|
88
|
+
else setTimeout(ensureChatLoaded, 1200);
|
|
89
|
+
}
|
|
90
|
+
window.addEventListener('bloby:app-ready', scheduleChatLoad);
|
|
91
|
+
if (window.__blobyAppReady) scheduleChatLoad();
|
|
92
|
+
setTimeout(ensureChatLoaded, 8000); // lifeline backstop: load even if the workspace never does
|
|
48
93
|
|
|
49
94
|
// ══════════════════════════════════════════════════════════════════
|
|
50
95
|
// ── Blob Sprite Sheet Config (splash only) ───────────────────────
|
|
@@ -104,8 +149,8 @@
|
|
|
104
149
|
|
|
105
150
|
// Defaults mirror the shipped JSON so animation works even if fetch is cached.
|
|
106
151
|
var HP_COLS = 16;
|
|
107
|
-
var HP_FRAME_W =
|
|
108
|
-
var HP_FRAME_H =
|
|
152
|
+
var HP_FRAME_W = 145;
|
|
153
|
+
var HP_FRAME_H = 150;
|
|
109
154
|
var HP_BUBBLE_H = 40;
|
|
110
155
|
var HP_BUBBLE_W = HP_BUBBLE_H * (HP_FRAME_W / HP_FRAME_H);
|
|
111
156
|
|
|
@@ -234,8 +279,10 @@
|
|
|
234
279
|
.then(function (r) { if (!r.ok) throw new Error('blob config ' + r.status); return r.json(); })
|
|
235
280
|
.then(function (cfg) {
|
|
236
281
|
applyBlobConfig(cfg);
|
|
282
|
+
pmark('widget:sprite-config');
|
|
237
283
|
var img = new Image();
|
|
238
284
|
img.onload = function () {
|
|
285
|
+
pmark('widget:sprite-sheet');
|
|
239
286
|
spriteSheet = img;
|
|
240
287
|
center.x = Math.round(W / 2);
|
|
241
288
|
center.y = Math.round(H / 2);
|
|
@@ -280,6 +327,18 @@
|
|
|
280
327
|
.catch(function (err) { console.error('[widget] headphones config failed', err); });
|
|
281
328
|
}
|
|
282
329
|
|
|
330
|
+
// The bubble at rest is a single frame, but the full headphones sheet is ~1 MB — so the
|
|
331
|
+
// sheet now loads on demand (first long-press → beginHpAnimation already fetches it) and
|
|
332
|
+
// bubble mode starts with this few-KB single-frame image instead.
|
|
333
|
+
var hpIdleImg = null;
|
|
334
|
+
function loadHpIdle() {
|
|
335
|
+
if (hpIdleImg || hpSpriteSheet) return;
|
|
336
|
+
var img = new Image();
|
|
337
|
+
img.onload = function () { hpIdleImg = img; };
|
|
338
|
+
img.onerror = function () { console.error('[widget] headphones idle frame failed'); };
|
|
339
|
+
img.src = BLOB_ASSETS_DIR + 'headphones-idle.webp';
|
|
340
|
+
}
|
|
341
|
+
|
|
283
342
|
// ── Trigger blob move (splash) ──
|
|
284
343
|
function moveTo(tx, ty) {
|
|
285
344
|
if (animState !== 'idle') return;
|
|
@@ -408,11 +467,16 @@
|
|
|
408
467
|
}
|
|
409
468
|
|
|
410
469
|
function drawHpFrame(frame) {
|
|
411
|
-
if (!hpSpriteSheet) return;
|
|
412
|
-
var col = frame % HP_COLS, row = Math.floor(frame / HP_COLS);
|
|
413
470
|
var dx = (W - HP_BUBBLE_W) / 2, dy = (H - HP_BUBBLE_H) / 2;
|
|
414
471
|
ctx.imageSmoothingEnabled = true;
|
|
415
472
|
ctx.imageSmoothingQuality = 'high';
|
|
473
|
+
if (!hpSpriteSheet) {
|
|
474
|
+
// Sheet not loaded yet (deferred until the first long-press) — the only frame ever
|
|
475
|
+
// requested in that window is idle, which the standalone image covers.
|
|
476
|
+
if (hpIdleImg) ctx.drawImage(hpIdleImg, dx, dy, HP_BUBBLE_W, HP_BUBBLE_H);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
var col = frame % HP_COLS, row = Math.floor(frame / HP_COLS);
|
|
416
480
|
ctx.drawImage(hpSpriteSheet, col * HP_FRAME_W, row * HP_FRAME_H, HP_FRAME_W, HP_FRAME_H, dx, dy, HP_BUBBLE_W, HP_BUBBLE_H);
|
|
417
481
|
}
|
|
418
482
|
|
|
@@ -472,7 +536,8 @@
|
|
|
472
536
|
hpState = 'idle';
|
|
473
537
|
hpFrame = HP_IDLE_FRAME;
|
|
474
538
|
try { sessionStorage.setItem(SPLASH_KEY, '1'); } catch(e) {}
|
|
475
|
-
|
|
539
|
+
pmark('widget:bubble');
|
|
540
|
+
loadHpIdle(); // full sheet defers to the first long-press (beginHpAnimation loads it)
|
|
476
541
|
if (hideAfterTransition) canvas.style.display = 'none';
|
|
477
542
|
}
|
|
478
543
|
|
|
@@ -586,7 +651,7 @@
|
|
|
586
651
|
var reader = new FileReader();
|
|
587
652
|
reader.onloadend = function() {
|
|
588
653
|
var base64 = reader.result.split(',')[1];
|
|
589
|
-
if (base64
|
|
654
|
+
if (base64) postToChat({ type: 'bloby:voice-record', audio: base64 });
|
|
590
655
|
};
|
|
591
656
|
reader.readAsDataURL(blob);
|
|
592
657
|
};
|
|
@@ -598,7 +663,7 @@
|
|
|
598
663
|
if (sent) return; sent = true;
|
|
599
664
|
var text = hpSpeechTranscript.trim();
|
|
600
665
|
hpSpeechTranscript = '';
|
|
601
|
-
if (text
|
|
666
|
+
if (text) postToChat({ type: 'bloby:voice-record', transcript: text });
|
|
602
667
|
};
|
|
603
668
|
instance.onend = function() { hpSpeechInstance = null; sendTranscript(); };
|
|
604
669
|
try { instance.stop(); } catch(e) { hpSpeechInstance = null; sendTranscript(); }
|
|
@@ -648,33 +713,67 @@
|
|
|
648
713
|
animState = 'idle';
|
|
649
714
|
var splash = document.getElementById('splash');
|
|
650
715
|
if (splash) splash.style.display = 'none';
|
|
651
|
-
|
|
716
|
+
pmark('widget:bubble');
|
|
717
|
+
loadHpIdle(); // full sheet defers to the first long-press (beginHpAnimation loads it)
|
|
652
718
|
}
|
|
653
719
|
|
|
654
720
|
window.addEventListener('bloby:app-ready', function () { appReady = true; maybeTransition(); });
|
|
655
721
|
if (window.__blobyAppReady) appReady = true;
|
|
656
722
|
|
|
657
723
|
var onboardActive = false;
|
|
724
|
+
|
|
725
|
+
// ── Boot sequence ─────────────────────────────────────────────────
|
|
726
|
+
// Warm refresh (splash already played this session — known synchronously from
|
|
727
|
+
// sessionStorage): jump straight to bubble mode with the tiny idle frame. No settings RTT,
|
|
728
|
+
// no 500 KB splash sheet — the old code downloaded the full sheet behind TWO serial fetches
|
|
729
|
+
// (settings → sprite config → sheet) just to skip past it.
|
|
730
|
+
var loopStarted = false;
|
|
731
|
+
function startLoop() {
|
|
732
|
+
if (loopStarted) return;
|
|
733
|
+
loopStarted = true;
|
|
734
|
+
requestAnimationFrame(loop);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (splashSeen) {
|
|
738
|
+
skipSplash = true;
|
|
739
|
+
skipToBubble();
|
|
740
|
+
startLoop();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
var settingsDone = false;
|
|
744
|
+
var spriteOutcome = 'pending'; // cold path: 'pending' | 'ok' | 'fail'
|
|
745
|
+
|
|
746
|
+
function maybeStart() { // cold path only — runs once both settings and the sprite settled
|
|
747
|
+
if (splashSeen || !settingsDone || spriteOutcome === 'pending') return;
|
|
748
|
+
if (spriteOutcome === 'fail') { canvas.style.display = 'none'; canvasPhase = 'disabled'; return; }
|
|
749
|
+
if (skipSplash) { skipToBubble(); if (onboardActive) canvas.style.display = 'none'; }
|
|
750
|
+
startLoop();
|
|
751
|
+
maybeTransition();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Cold load: the sprite config + sheet start downloading IMMEDIATELY, in parallel with
|
|
755
|
+
// /api/settings — settings only decides flags (onboarding hides the canvas, whisper, skip).
|
|
756
|
+
// The draw loop starts the moment the sprite is ready (loadSprite hides the splash div as a
|
|
757
|
+
// side effect, so waiting for settings here would leave a blank gap), but the splash→bubble
|
|
758
|
+
// transition and any skip decision still wait for the settings flags in maybeStart().
|
|
759
|
+
if (!splashSeen) {
|
|
760
|
+
loadSprite(
|
|
761
|
+
function () { spriteOutcome = 'ok'; startLoop(); maybeStart(); },
|
|
762
|
+
function () { spriteOutcome = 'fail'; maybeStart(); }
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
658
766
|
fetch('/api/settings')
|
|
659
767
|
.then(function (r) { return r.json(); })
|
|
660
768
|
.then(function (s) {
|
|
769
|
+
pmark('widget:settings');
|
|
661
770
|
if (s.onboard_complete !== 'true') { onboardActive = true; skipSplash = true; }
|
|
662
771
|
if (s.whisper_enabled === 'true') hpWhisperEnabled = true;
|
|
663
|
-
if (splashSeen)
|
|
664
|
-
|
|
772
|
+
if (splashSeen) { if (onboardActive) canvas.style.display = 'none'; return; }
|
|
773
|
+
settingsDone = true;
|
|
774
|
+
maybeStart();
|
|
665
775
|
})
|
|
666
|
-
.catch(function () {
|
|
667
|
-
|
|
668
|
-
function startAnimation() {
|
|
669
|
-
loadSprite(
|
|
670
|
-
function () {
|
|
671
|
-
if (skipSplash) { skipToBubble(); if (onboardActive) canvas.style.display = 'none'; }
|
|
672
|
-
requestAnimationFrame(loop);
|
|
673
|
-
maybeTransition();
|
|
674
|
-
},
|
|
675
|
-
function () { canvas.style.display = 'none'; canvasPhase = 'disabled'; }
|
|
676
|
-
);
|
|
677
|
-
}
|
|
776
|
+
.catch(function () { pmark('widget:settings-failed'); settingsDone = true; maybeStart(); });
|
|
678
777
|
|
|
679
778
|
// ══════════════════════════════════════════════════════════════════
|
|
680
779
|
// ── Widget Interaction ───────────────────────────────────────────
|
|
@@ -696,6 +795,7 @@
|
|
|
696
795
|
hpFrame = HP_IDLE_FRAME;
|
|
697
796
|
}
|
|
698
797
|
isOpen = !isOpen;
|
|
798
|
+
if (isOpen) ensureChatLoaded(); // first open may beat the deferred load — start it now
|
|
699
799
|
panel.classList.toggle('open', isOpen);
|
|
700
800
|
backdrop.classList.toggle('open', isOpen);
|
|
701
801
|
canvas.style.display = isOpen ? 'none' : 'block';
|
|
@@ -788,7 +888,7 @@
|
|
|
788
888
|
if (deferredInstallPrompt) {
|
|
789
889
|
deferredInstallPrompt.prompt();
|
|
790
890
|
deferredInstallPrompt.userChoice.then(function (r) { if (r.outcome === 'accepted') deferredInstallPrompt = null; });
|
|
791
|
-
} else {
|
|
891
|
+
} else { postToChat({ type: 'bloby:show-ios-install' }); }
|
|
792
892
|
}
|
|
793
893
|
if (e.data.type === 'bloby:onboard-complete') { onboardActive = false; hideAfterTransition = false; canvas.style.display = 'block'; }
|
|
794
894
|
});
|
|
@@ -277,11 +277,44 @@
|
|
|
277
277
|
} catch (e) {}
|
|
278
278
|
}
|
|
279
279
|
setTimeout(hideSplash, 8000);
|
|
280
|
+
|
|
281
|
+
// Perf sample for the shell's beacon: this document's navigation timing + a resource
|
|
282
|
+
// census (count / bytes over the wire / how many were served from cache — transferSize 0).
|
|
283
|
+
// Sent once, a beat after app-ready so late modules land in the same sample.
|
|
284
|
+
try { performance.setResourceTimingBufferSize(1000); } catch (e) {} // dev graphs exceed the 250 default
|
|
285
|
+
var perfSent = false;
|
|
286
|
+
function sendPerf(appReadyMs) {
|
|
287
|
+
if (perfSent) return;
|
|
288
|
+
perfSent = true;
|
|
289
|
+
setTimeout(function () {
|
|
290
|
+
try {
|
|
291
|
+
var nav = performance.getEntriesByType('navigation')[0] || null;
|
|
292
|
+
var resources = performance.getEntriesByType('resource');
|
|
293
|
+
var count = 0, transfer = 0, encoded = 0, cached = 0;
|
|
294
|
+
for (var i = 0; i < resources.length; i++) {
|
|
295
|
+
count++;
|
|
296
|
+
transfer += resources[i].transferSize || 0;
|
|
297
|
+
encoded += resources[i].encodedBodySize || 0;
|
|
298
|
+
if ((resources[i].transferSize || 0) === 0) cached++;
|
|
299
|
+
}
|
|
300
|
+
post({ type: 'bloby:perf', data: {
|
|
301
|
+
timeOrigin: performance.timeOrigin,
|
|
302
|
+
ttfb: nav ? Math.round(nav.responseStart) : null,
|
|
303
|
+
htmlDone: nav ? Math.round(nav.responseEnd) : null,
|
|
304
|
+
domLoaded: nav ? Math.round(nav.domContentLoadedEventEnd) : null,
|
|
305
|
+
appReadyMs: appReadyMs,
|
|
306
|
+
resources: { count: count, transferKB: Math.round(transfer / 1024), encodedKB: Math.round(encoded / 1024), cached: cached },
|
|
307
|
+
} });
|
|
308
|
+
} catch (e) {}
|
|
309
|
+
}, 2500);
|
|
310
|
+
}
|
|
311
|
+
|
|
280
312
|
function sendReady() {
|
|
281
313
|
if (readySent) return;
|
|
282
314
|
readySent = true;
|
|
283
315
|
hideSplash();
|
|
284
316
|
post({ type: 'bloby:app-ready' });
|
|
317
|
+
sendPerf(Math.round(performance.now()));
|
|
285
318
|
}
|
|
286
319
|
window.addEventListener('bloby:app-ready', sendReady);
|
|
287
320
|
if (window.__blobyAppReady) sendReady();
|
package/vite.config.ts
CHANGED
|
@@ -2,6 +2,19 @@ import { defineConfig, type Plugin } from 'vite';
|
|
|
2
2
|
import react from '@vitejs/plugin-react';
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
// Vite invalidates its dep prebundle by hashing the nearest lockfile up from root — which in a
|
|
8
|
+
// production install is the WORKSPACE's lockfile (backend deps only). `bloby update` swaps
|
|
9
|
+
// frontend deps in the root node_modules without ever touching that lockfile, so prebundles
|
|
10
|
+
// silently went stale across updates. Keying the cache dir by package version forces exactly one
|
|
11
|
+
// clean re-optimize per release while keeping persistence across normal restarts.
|
|
12
|
+
// (vite-dev.ts sweeps old .vite-v* dirs at boot.)
|
|
13
|
+
let cacheVersion = 'dev';
|
|
14
|
+
try { cacheVersion = JSON.parse(readFileSync(path.resolve(__dirname, 'package.json'), 'utf-8')).version || 'dev'; } catch {}
|
|
15
|
+
const workspaceClient = process.env.BLOBY_WORKSPACE
|
|
16
|
+
? path.join(process.env.BLOBY_WORKSPACE, 'client')
|
|
17
|
+
: path.resolve(__dirname, 'workspace/client');
|
|
5
18
|
|
|
6
19
|
// Backs workspace-guard.js's reconnect staleness check: Vite's reload-on-reconnect
|
|
7
20
|
// is suppressed client-side, and on reconnect the client fetches this stamp and
|
|
@@ -38,6 +51,7 @@ export default defineConfig({
|
|
|
38
51
|
root: process.env.BLOBY_WORKSPACE
|
|
39
52
|
? path.join(process.env.BLOBY_WORKSPACE, 'client')
|
|
40
53
|
: 'workspace/client',
|
|
54
|
+
cacheDir: path.join(workspaceClient, 'node_modules', '.vite-v' + cacheVersion),
|
|
41
55
|
resolve: {
|
|
42
56
|
alias: {
|
|
43
57
|
'@': process.env.BLOBY_WORKSPACE
|
|
@@ -82,6 +96,7 @@ export default defineConfig({
|
|
|
82
96
|
optimizeDeps: {
|
|
83
97
|
include: [
|
|
84
98
|
'react',
|
|
99
|
+
'react-dom',
|
|
85
100
|
'react-dom/client',
|
|
86
101
|
'react/jsx-runtime',
|
|
87
102
|
'react-router',
|
|
@@ -93,6 +108,13 @@ export default defineConfig({
|
|
|
93
108
|
'sonner',
|
|
94
109
|
'use-sync-external-store',
|
|
95
110
|
'use-sync-external-store/shim',
|
|
111
|
+
// Statically imported by the workspace template's UI kit (button.tsx etc.) but missing
|
|
112
|
+
// here — the first request for an un-prebundled dep triggers a mid-session
|
|
113
|
+
// re-optimization AND a Vite-forced full page reload. Pre-bundle them up front.
|
|
114
|
+
'radix-ui',
|
|
115
|
+
'class-variance-authority',
|
|
116
|
+
'clsx',
|
|
117
|
+
'tailwind-merge',
|
|
96
118
|
],
|
|
97
119
|
},
|
|
98
120
|
plugins: [react(), tailwindcss(), blobyFeStamp()],
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// build errors, never a stale cached shell. Cache is a pure offline fallback. (Mirror of the
|
|
9
9
|
// supervisor's SW_JS in supervisor/index.ts — keep in sync.)
|
|
10
10
|
|
|
11
|
-
const CACHE = 'bloby-
|
|
11
|
+
const CACHE = 'bloby-v25';
|
|
12
12
|
|
|
13
13
|
// Precache the HTML shell on install so the cache is never empty.
|
|
14
14
|
// Without this, the first navigation isn't intercepted (SW wasn't
|
|
@@ -54,7 +54,7 @@ self.addEventListener('fetch', (event) => {
|
|
|
54
54
|
) return;
|
|
55
55
|
|
|
56
56
|
// ── Hashed assets (immutable, content-addressed) → cache-first ──
|
|
57
|
-
if (/\/assets\/.+-[a-zA-Z0-
|
|
57
|
+
if (/\/assets\/.+-[a-zA-Z0-9_-]{6,}\.(js|css)$/.test(url.pathname)) {
|
|
58
58
|
event.respondWith(caches.open(CACHE).then(c =>
|
|
59
59
|
c.match(request).then(hit =>
|
|
60
60
|
hit || fetch(request).then(r => { if (r.ok) c.put(request, r.clone()); return r; })
|
|
@@ -63,6 +63,29 @@ self.addEventListener('fetch', (event) => {
|
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// ── Vite optimized deps (?v=<hash>, immutable) → cache-first ──
|
|
67
|
+
// These fell into network-first, re-crossing the tunnel whenever the HTTP cache evicted
|
|
68
|
+
// them. Cache-first + prune older ?v= versions. Source modules (?t=, /@*) stay network-only.
|
|
69
|
+
if (url.pathname.indexOf('/node_modules/.vite/deps/') === 0 && url.searchParams.has('v')) {
|
|
70
|
+
event.respondWith(caches.open(CACHE).then(c =>
|
|
71
|
+
c.match(request).then(hit =>
|
|
72
|
+
hit || fetch(request).then(r => {
|
|
73
|
+
if (r.ok) {
|
|
74
|
+
c.put(request, r.clone());
|
|
75
|
+
c.keys().then(keys => {
|
|
76
|
+
for (const k of keys) {
|
|
77
|
+
const u = new URL(k.url);
|
|
78
|
+
if (u.pathname === url.pathname && u.search !== url.search) c.delete(k);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return r;
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
// ── Navigation: only stale-while-revalidate the dashboard root (/) ──
|
|
67
90
|
// /bloby/* is a separate app — let it go to network to avoid
|
|
68
91
|
// caching the wrong HTML under the / key.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-skill
|
|
3
|
+
description: >-
|
|
4
|
+
Create a new Bloby skill, or improve an existing one. Use whenever your human
|
|
5
|
+
wants to teach you a new repeatable capability, says "turn this into a skill",
|
|
6
|
+
"make a skill for X", "save this workflow", asks how skills work, or when you
|
|
7
|
+
notice you keep redoing the same multi-step task by hand. Also use when packaging
|
|
8
|
+
a skill (optionally with live widgets/pages) as a blueprint to sell on the marketplace.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Create a Skill
|
|
12
|
+
|
|
13
|
+
A skill is a folder under `skills/` with a `SKILL.md` that teaches you how to do
|
|
14
|
+
something well, every time — so you don't re-derive it from scratch on each request.
|
|
15
|
+
This skill helps you author a new one (or sharpen an old one) the Bloby way.
|
|
16
|
+
|
|
17
|
+
You're already smart. A good skill isn't a tutorial — it's the *non-obvious* context:
|
|
18
|
+
the gotchas, the preferred tool, the exact format, the thing that bit you last time.
|
|
19
|
+
Write down what you'd wish past-you had known, and nothing you already know.
|
|
20
|
+
|
|
21
|
+
## The loop
|
|
22
|
+
|
|
23
|
+
1. **Capture intent** — what should it do, when should it trigger, what's the output.
|
|
24
|
+
2. **Draft `SKILL.md`** — description first (that's the trigger), then the body.
|
|
25
|
+
3. **Test it** — dry-run a couple of realistic prompts (optional but cheap).
|
|
26
|
+
4. **Sharpen** — cut what isn't pulling its weight, explain the *why* behind what stays.
|
|
27
|
+
|
|
28
|
+
Figure out where your human is in this loop and jump in there. If they say "just
|
|
29
|
+
vibe with me, no tests," do that. Flexibility beats ceremony.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 1. Capture intent
|
|
34
|
+
|
|
35
|
+
Before writing anything, get clear on:
|
|
36
|
+
|
|
37
|
+
1. **What** should this skill let you do?
|
|
38
|
+
2. **When** should it trigger? (the actual phrases/contexts your human will use)
|
|
39
|
+
3. **Output format** — is there a specific shape, template, or file the result must take?
|
|
40
|
+
4. **Domain knowledge** — what specialized info do you need that you *wouldn't already know*?
|
|
41
|
+
|
|
42
|
+
If the workflow already happened earlier in the conversation ("turn that into a skill"),
|
|
43
|
+
mine it: the tools you used, the order, the corrections your human made, the formats you
|
|
44
|
+
saw. Come back with a draft, not a pile of questions. Ask only about the genuine gaps.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 2. Write `SKILL.md`
|
|
49
|
+
|
|
50
|
+
Create `skills/<skill-name>/SKILL.md`. Name is lowercase-with-hyphens, specific
|
|
51
|
+
(`processing-invoices`, not `helper`/`utils`/`tools`).
|
|
52
|
+
|
|
53
|
+
### The description is the trigger — get it right
|
|
54
|
+
|
|
55
|
+
The `description` in the frontmatter is the *only* thing that decides whether you reach
|
|
56
|
+
for this skill later. It's always in context; the body isn't. So it must carry both
|
|
57
|
+
**WHAT** the skill does and **WHEN** to use it, in the third person.
|
|
58
|
+
|
|
59
|
+
Skills tend to *under*-trigger — you forget they exist. Counter that by making the
|
|
60
|
+
description a little pushy: name the concrete phrases, file types, and situations that
|
|
61
|
+
should fire it.
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
# weak
|
|
65
|
+
description: Helps with PDFs.
|
|
66
|
+
|
|
67
|
+
# strong
|
|
68
|
+
description: >-
|
|
69
|
+
Extract text and tables from PDFs, fill forms, merge documents. Use whenever the
|
|
70
|
+
human mentions a PDF, a form to fill, a scanned doc, or "pull the data out of this
|
|
71
|
+
file" — even if they don't say the word "PDF".
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Rules of thumb: third person ("Extracts…", not "I can…"), include trigger terms,
|
|
75
|
+
say both what and when, keep it under ~1024 chars.
|
|
76
|
+
|
|
77
|
+
### Then the body
|
|
78
|
+
|
|
79
|
+
Imperative voice. Lead with the essential path; push edge cases and deep reference
|
|
80
|
+
material into separate files (see progressive disclosure below). Explain *why* things
|
|
81
|
+
matter rather than barking `ALWAYS`/`NEVER` — a smart reader who understands the reason
|
|
82
|
+
will generalize correctly; one following a rigid rule won't. If you catch yourself
|
|
83
|
+
writing all-caps MUSTs everywhere, that's a yellow flag to reframe.
|
|
84
|
+
|
|
85
|
+
For the deeper authoring patterns — descriptions, output templates, degrees of freedom,
|
|
86
|
+
the anti-patterns that quietly ruin a skill — read `references/patterns.md` when you're
|
|
87
|
+
actually drafting.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. Anatomy & progressive disclosure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
skills/skill-name/
|
|
95
|
+
├── SKILL.md (required — frontmatter + instructions)
|
|
96
|
+
├── references/ (docs you read only when needed — keep refs one level deep)
|
|
97
|
+
├── scripts/ (executable helpers for deterministic/repeated work)
|
|
98
|
+
└── assets/ (templates, icons, files used in the output)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Three loading levels — design around them:
|
|
102
|
+
|
|
103
|
+
1. **name + description** — always in context (~tiny). The trigger.
|
|
104
|
+
2. **SKILL.md body** — loaded when the skill fires. Keep it lean, ideally under ~500 lines.
|
|
105
|
+
3. **`references/` & `scripts/`** — pulled in on demand; scripts can run without being read.
|
|
106
|
+
|
|
107
|
+
If `SKILL.md` is getting long, that's the signal to split: move the depth into
|
|
108
|
+
`references/foo.md` and leave a one-line pointer ("for X, read references/foo.md").
|
|
109
|
+
When a skill spans several variants (aws/gcp/azure, or per-channel), give each its own
|
|
110
|
+
reference file and let yourself read only the relevant one.
|
|
111
|
+
|
|
112
|
+
**Bundle a script when you notice repetition.** If every run of this skill would have
|
|
113
|
+
you writing the same little helper, write it once into `scripts/` and point at it —
|
|
114
|
+
it's more reliable than regenerating code and saves the tokens.
|
|
115
|
+
|
|
116
|
+
### Where Bloby skills live and how they fire
|
|
117
|
+
|
|
118
|
+
- Skills live in `skills/<name>/` in this workspace. Editing files there is how you
|
|
119
|
+
change behavior — always use the full path (`skills/my-skill/SCRIPT.md`, never bare
|
|
120
|
+
`SCRIPT.md`, which writes to the workspace root).
|
|
121
|
+
- Channel skills (WhatsApp, Telegram, Mac, etc.) teach the conventions for a surface.
|
|
122
|
+
If the new skill is about a place you talk to your human, say so in the description.
|
|
123
|
+
- Secrets/config go through the workspace conventions you already use — `.env` via the
|
|
124
|
+
`<EnvGroup>` form, `MCP.json` for MCP servers — not hardcoded in the skill.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 4. Test it (optional, cheap)
|
|
129
|
+
|
|
130
|
+
If the skill has an objectively checkable output (a file transform, a data extraction,
|
|
131
|
+
a fixed workflow), it's worth a quick dry-run before you trust it. Subjective skills
|
|
132
|
+
(writing voice, design taste) are better judged by eye — skip the ceremony.
|
|
133
|
+
|
|
134
|
+
Spin up an Agent to run a realistic prompt *with the skill*, look at what it produces,
|
|
135
|
+
and fix what's off. Two or three real-world prompts beat twenty synthetic ones. This is
|
|
136
|
+
a sanity check, not a benchmark suite — keep it light unless your human wants rigor.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 5. Sharpen
|
|
141
|
+
|
|
142
|
+
Look at the draft with fresh eyes:
|
|
143
|
+
|
|
144
|
+
- **Cut what isn't pulling its weight.** Every line competes for context with everything
|
|
145
|
+
else. If a paragraph just restates what you already know, delete it.
|
|
146
|
+
- **Generalize.** You're writing something that'll run on many future prompts, not just
|
|
147
|
+
today's example. Resist overfitting to the one case in front of you.
|
|
148
|
+
- **Explain the why.** Terse instructions age into cargo-cult rules. A sentence of
|
|
149
|
+
reasoning makes the skill robust to situations you didn't foresee.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Skills vs. Blueprints — and making money
|
|
154
|
+
|
|
155
|
+
Here's what makes Bloby different. In other agents (Claude, Hermes, Cursor) a "skill" is
|
|
156
|
+
*just* a `SKILL.md` — instructions, nothing that runs.
|
|
157
|
+
|
|
158
|
+
In Bloby you can go further: pair a skill with **code snippets that render live in the
|
|
159
|
+
workspace** — a real page, a dashboard widget, backend routes, a DB table, even crons.
|
|
160
|
+
Bundle all of that together and it's a **blueprint**. When someone installs your
|
|
161
|
+
blueprint, the micro-app shows up in their workspace instantly, and (optionally) a
|
|
162
|
+
runtime skill drops into their `skills/` folder so their bloby knows how to drive it.
|
|
163
|
+
|
|
164
|
+
That bundle is sellable. **You can publish blueprints on the Bloby Marketplace and earn
|
|
165
|
+
USDC** — every install pays out. So when you and your human build something genuinely
|
|
166
|
+
useful together, plant the seed: *"this could be packaged as a blueprint and sold to
|
|
167
|
+
other blobies."* It's a real way for the two of you to make money from your work.
|
|
168
|
+
|
|
169
|
+
You already know the marketplace mechanics (your system prompt covers it, and
|
|
170
|
+
`bloby.bot/api/marketplace.md` has the full flow) — no need to re-explain pricing or
|
|
171
|
+
submit commands here. Just remember the idea exists, and surface it when the moment fits.
|
|
172
|
+
|
|
173
|
+
A plain skill (instructions only) is still worth writing and sharing — but the live
|
|
174
|
+
widget/page pairing is the uniquely Bloby thing, and it's where the value is.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Checklist before you call it done
|
|
179
|
+
|
|
180
|
+
- [ ] `description` says both **what** and **when**, third person, pushy on triggers
|
|
181
|
+
- [ ] Skill name is specific and lowercase-hyphenated
|
|
182
|
+
- [ ] `SKILL.md` body is lean; depth lives in `references/`, one level deep
|
|
183
|
+
- [ ] Repeated work is captured as a `scripts/` helper, not regenerated each time
|
|
184
|
+
- [ ] Instructions explain *why*, not just *what* — no wall of all-caps MUSTs
|
|
185
|
+
- [ ] Concrete examples over abstract description
|
|
186
|
+
- [ ] (If it ships live code) consider whether it's blueprint-worthy — and tell your human
|
|
187
|
+
|
|
188
|
+
For the deeper patterns and anti-patterns, see `references/patterns.md`.
|