bloby-bot 0.69.5 → 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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/supervisor/channels/whatsapp.ts +25 -22
  3. package/supervisor/index.ts +224 -20
  4. package/supervisor/public/morphy/headphones-idle.webp +0 -0
  5. package/supervisor/public/morphy/headphones.json +4 -4
  6. package/supervisor/public/morphy/headphones.webp +0 -0
  7. package/supervisor/public/morphy/teleporting.json +2 -2
  8. package/supervisor/public/morphy/teleporting.webp +0 -0
  9. package/supervisor/shell.ts +53 -0
  10. package/supervisor/vite-dev.ts +15 -1
  11. package/supervisor/widget.js +124 -24
  12. package/supervisor/workspace-guard.js +33 -0
  13. package/vite.config.ts +22 -0
  14. package/workspace/client/public/morphy/headphones-idle.webp +0 -0
  15. package/workspace/client/public/morphy/headphones.json +4 -4
  16. package/workspace/client/public/morphy/headphones.webp +0 -0
  17. package/workspace/client/public/morphy/teleporting.json +2 -2
  18. package/workspace/client/public/morphy/teleporting.webp +0 -0
  19. package/workspace/client/public/sw.js +25 -2
  20. package/workspace/skills/create-skill/SKILL.md +188 -0
  21. package/workspace/skills/create-skill/references/patterns.md +126 -0
  22. package/workspace/client/public/arrow.png +0 -0
  23. package/workspace/client/public/bloby_happy.mov +0 -0
  24. package/workspace/client/public/bloby_happy.webm +0 -0
  25. package/workspace/client/public/bloby_happy_reappearing.mov +0 -0
  26. package/workspace/client/public/bloby_happy_reappearing.webm +0 -0
  27. package/workspace/client/public/bloby_say_hi.mov +0 -0
  28. package/workspace/client/public/bloby_say_hi.webm +0 -0
  29. package/workspace/client/public/bloby_tilts.webm +0 -0
  30. package/workspace/client/public/headphones_spritesheet.webp +0 -0
  31. package/workspace/client/public/spritesheet.webp +0 -0
@@ -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 = 180;
108
- var HP_FRAME_H = 180;
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
- loadHpSprite();
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 && iframe.contentWindow) iframe.contentWindow.postMessage({ type: 'bloby:voice-record', audio: base64 }, location.origin);
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 && iframe.contentWindow) iframe.contentWindow.postMessage({ type: 'bloby:voice-record', transcript: text }, location.origin);
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
- loadHpSprite();
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) skipSplash = true;
664
- startAnimation();
772
+ if (splashSeen) { if (onboardActive) canvas.style.display = 'none'; return; }
773
+ settingsDone = true;
774
+ maybeStart();
665
775
  })
666
- .catch(function () { if (splashSeen) skipSplash = true; startAnimation(); });
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 { iframe.contentWindow.postMessage({ type: 'bloby:show-ios-install' }, location.origin); }
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()],
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "headphones",
3
- "spritesheet": "headphones.png",
3
+ "spritesheet": "headphones.webp",
4
4
  "frame": {
5
- "w": 174,
6
- "h": 180
5
+ "w": 145,
6
+ "h": 150
7
7
  },
8
8
  "grid": {
9
9
  "cols": 16,
@@ -37,4 +37,4 @@
37
37
  "next": "idle"
38
38
  }
39
39
  }
40
- }
40
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teleporting",
3
- "spritesheet": "teleporting.png",
3
+ "spritesheet": "teleporting.webp",
4
4
  "frame": {
5
5
  "w": 218,
6
6
  "h": 180
@@ -35,4 +35,4 @@
35
35
  "next": "idle"
36
36
  }
37
37
  }
38
- }
38
+ }
@@ -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-v24';
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-9]{6,}\.(js|css)$/.test(url.pathname)) {
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`.