bloby-bot 0.69.6 → 0.70.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.
package/README.md CHANGED
@@ -133,7 +133,7 @@ workspace/
133
133
  PULSE.json Periodic wake-up config (interval, quiet hours)
134
134
  CRONS.json Scheduled tasks with cron expressions
135
135
  memory/ Daily notes (YYYY-MM-DD.md files, append-only)
136
- skills/ Plugin directories with .claude-plugin/plugin.json
136
+ skills/ Skill folders (SKILL.md with name+description frontmatter)
137
137
  MCP.json MCP server configuration (optional)
138
138
  files/ Attachment storage (audio, images, documents)
139
139
  ```
@@ -193,9 +193,9 @@ When a cron or pulse fires:
193
193
 
194
194
  ---
195
195
 
196
- ## Skills & Plugins
196
+ ## Skills
197
197
 
198
- The agent auto-discovers skills in `workspace/skills/`. Each skill is a folder containing `.claude-plugin/plugin.json`. These are loaded as tool plugins when the agent starts a query.
198
+ The agent auto-discovers skills in `workspace/skills/`. Each skill is a folder containing a `SKILL.md` whose YAML frontmatter carries `name` (= folder name) and `description`. All three harnesses surface that metadata in context and load the body on demand: the Claude harness mirrors skills into `workspace/.claude/skills` and enables them via the Agent SDK's `skills` option, the Codex harness mirrors them into `workspace/.codex/skills` and primes them via `skills/list`, and the Pi harness injects a name+description index into the system prompt (see `supervisor/harnesses/skills.ts`).
199
199
 
200
200
  MCP servers can be configured in `workspace/MCP.json`. The agent loads them at query time and logs which servers are active.
201
201
 
@@ -568,7 +568,7 @@ workspace/
568
568
  PULSE.json Periodic wake-up configuration
569
569
  CRONS.json Scheduled task definitions
570
570
  memory/ Daily notes (YYYY-MM-DD.md)
571
- skills/ Plugin directories (.claude-plugin/plugin.json)
571
+ skills/ Skill folders (SKILL.md with frontmatter)
572
572
  MCP.json MCP server configuration (optional)
573
573
  files/ Uploaded file storage (audio/, images/, documents/)
574
574
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.69.6",
3
+ "version": "0.70.1",
4
4
  "releaseNotes": [
5
5
  "1. Fix: agent self-update ",
6
6
  "1",
@@ -631,7 +631,7 @@ function BlobyApp() {
631
631
  <div className="absolute inset-0 z-10 flex items-center justify-center bg-background/70 backdrop-blur-[2px]">
632
632
  <span className="flex items-center gap-2 text-[12px] text-muted-foreground bg-white/[0.05] border border-white/[0.08] rounded-full px-3.5 py-1.5 shadow-lg">
633
633
  <span className={`h-1.5 w-1.5 rounded-full animate-pulse ${updating ? 'bg-amber-400' : 'bg-red-500'}`} />
634
- {updating ? 'Updating back in a moment…' : 'Offline waiting for connection…'}
634
+ {updating ? 'Morphy is Updating…' : 'Morphy is Offline'}
635
635
  </span>
636
636
  </div>
637
637
  )}
@@ -20,6 +20,7 @@ import { getClaudeAccessToken } from '../../worker/claude-auth.js';
20
20
  import { assembleSystemPrompt } from '../../worker/prompts/prompt-assembler.js';
21
21
  import { buildAgents } from '../agents/index.js';
22
22
  import { preWarm, claimWarmup, discardWarmup } from '../cli-warmup.js';
23
+ import { mirrorSkillsInto } from './skills.js';
23
24
 
24
25
  // ── Types ──────────────────────────────────────────────────────────────────
25
26
 
@@ -128,6 +129,20 @@ function formatConversationHistory(messages: RecentMessage[]): string {
128
129
  return messages.map((m) => `${m.role}: ${m.content}`).join('\n\n');
129
130
  }
130
131
 
132
+ // The Agent SDK discovers project-scope skills under `<cwd>/.claude/skills`
133
+ // (name+description listed in context, body lazy-loaded via the Skill tool).
134
+ // Bloby keeps the canonical skills in `workspace/skills/<name>`, so we mirror
135
+ // each one into `.claude/skills/<name>` as a symlink — same single-source
136
+ // pattern as the codex harness's `.codex/skills` mirror. The returned names
137
+ // feed the `skills` option as an explicit allowlist: only Bloby's workspace
138
+ // skills are enabled, so the human's personal `~/.claude/skills` never leak
139
+ // into the agent and the option hash stays deterministic for the pre-warmer.
140
+ const CLAUDE_SKILLS_ROOT = path.join(WORKSPACE_DIR, '.claude', 'skills');
141
+
142
+ function syncClaudeSkills(): string[] {
143
+ return mirrorSkillsInto(CLAUDE_SKILLS_ROOT, 'claude');
144
+ }
145
+
131
146
  /** Load MCP server config from workspace/MCP.json */
132
147
  function loadMcpServers(): Record<string, any> | undefined {
133
148
  try {
@@ -210,6 +225,7 @@ async function buildConversationOptions(
210
225
 
211
226
  const agents = buildAgents();
212
227
  const mcpServers = loadMcpServers();
228
+ const skills = syncClaudeSkills();
213
229
 
214
230
  return {
215
231
  model,
@@ -224,6 +240,7 @@ async function buildConversationOptions(
224
240
  systemPrompt,
225
241
  mcpServers,
226
242
  agents,
243
+ skills,
227
244
  agentProgressSummaries: true,
228
245
  // Auto-compaction: the live conversation is a single long-lived query() whose
229
246
  // context grows every turn (messages + tool results + sub-agent transcripts).
@@ -294,6 +311,8 @@ export async function startConversation(
294
311
  if (baseOptions.mcpServers) {
295
312
  log.info(`[conversation] MCP servers: ${Object.keys(baseOptions.mcpServers).join(', ')}`);
296
313
  }
314
+ const skillNames = Array.isArray(baseOptions.skills) ? baseOptions.skills : [];
315
+ log.info(`[conversation] Skills: ${skillNames.length ? skillNames.join(', ') : 'none'}`);
297
316
 
298
317
  // Try to claim a pre-warmed subprocess — its abortController is the one
299
318
  // baked into the warm query and must be reused for end/abort to reach it.
@@ -667,6 +686,10 @@ export async function startBlobyAgentQuery(
667
686
  maxTurns: effectiveMaxTurns,
668
687
  abortController,
669
688
  systemPrompt: enrichedPrompt,
689
+ // Customer-facing runs (supportPrompt) get no skills — SCRIPT.md alone
690
+ // governs that persona and the listing would just leak ops docs into
691
+ // customer context. Owner runs (pulse/cron) get the full workspace set.
692
+ skills: supportPrompt ? [] : syncClaudeSkills(),
670
693
  mcpServers,
671
694
  stderr: (chunk: string) => { stderrBuf += chunk; },
672
695
  env: {
@@ -781,6 +804,7 @@ export async function runAgentQuery(req: AgentQueryRequest): Promise<AgentQueryR
781
804
  maxTurns,
782
805
  abortController,
783
806
  systemPrompt: systemPrompt as any,
807
+ skills: syncClaudeSkills(),
784
808
  ...(req.sessionId ? { resume: req.sessionId } : {}),
785
809
  stderr: (chunk: string) => { stderrBuf += chunk; },
786
810
  env: {
@@ -35,6 +35,7 @@ import { WORKSPACE_DIR } from '../../shared/paths.js';
35
35
  import type { SavedFile } from '../file-saver.js';
36
36
  import { getCodexAccessToken } from '../../worker/codex-auth.js';
37
37
  import { assembleSystemPrompt } from '../../worker/prompts/prompt-assembler.js';
38
+ import { mirrorSkillsInto } from './skills.js';
38
39
  import type { OnAgentMessage, RecentMessage, AgentAttachment, AgentQueryRequest, AgentQueryResult } from './types.js';
39
40
  export type { RecentMessage, AgentAttachment };
40
41
 
@@ -771,7 +772,6 @@ async function spawnAndInitialize(
771
772
  }
772
773
  }
773
774
 
774
- const SKILLS_DIR = path.join(WORKSPACE_DIR, 'skills');
775
775
  // Codex discovers "repo"-scope skills under `<cwd>/.codex/skills` (verified
776
776
  // against 0.135.0 — a bare `<cwd>/skills` is NOT scanned, and `skills/list`
777
777
  // has no extra-root param). Bloby keeps the canonical skills in
@@ -780,33 +780,8 @@ const SKILLS_DIR = path.join(WORKSPACE_DIR, 'skills');
780
780
  // (Each SKILL.md needs YAML frontmatter or codex rejects it — see SKILL_FORMAT_MIGRATION.md.)
781
781
  const CODEX_SKILLS_ROOT = path.join(WORKSPACE_DIR, '.codex', 'skills');
782
782
 
783
- /** Mirror workspace/skills/<name> → workspace/.codex/skills/<name> as symlinks (idempotent). */
784
- function syncCodexSkillRoot(): void {
785
- let names: string[] = [];
786
- try {
787
- names = fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
788
- .filter((e) => e.isDirectory() || e.isSymbolicLink())
789
- .map((e) => e.name);
790
- } catch {
791
- return; // no skills dir — nothing to mirror
792
- }
793
- try { fs.mkdirSync(CODEX_SKILLS_ROOT, { recursive: true }); } catch {}
794
- for (const name of names) {
795
- const target = path.join(SKILLS_DIR, name);
796
- const link = path.join(CODEX_SKILLS_ROOT, name);
797
- try {
798
- const cur = fs.existsSync(link) ? fs.realpathSync(link) : null;
799
- if (cur === fs.realpathSync(target)) continue; // already correct
800
- try { fs.rmSync(link, { recursive: true, force: true }); } catch {}
801
- fs.symlinkSync(target, link, 'dir');
802
- } catch (err: any) {
803
- log.warn(`[codex] could not mirror skill "${name}" into .codex/skills: ${err.message}`);
804
- }
805
- }
806
- }
807
-
808
783
  function primeWorkspaceSkills(rpc: CodexRpc): void {
809
- syncCodexSkillRoot();
784
+ mirrorSkillsInto(CODEX_SKILLS_ROOT, 'codex');
810
785
  rpc.request('skills/list', {
811
786
  cwds: [WORKSPACE_DIR],
812
787
  forceReload: true,
@@ -24,6 +24,7 @@ import type {
24
24
  } from '../types.js';
25
25
  export type { RecentMessage, AgentAttachment };
26
26
 
27
+ import { buildSkillsIndex } from '../skills.js';
27
28
  import { createAsyncQueue, type AsyncQueue } from './async-queue.js';
28
29
  import { createPiSession, type PiSessionEvent } from './session.js';
29
30
  import { getPiSubProvider } from './sub-providers.js';
@@ -110,6 +111,11 @@ async function buildSystemPrompt(
110
111
  const basePrompt = await assembleSystemPrompt(names?.botName, names?.humanName, 'pi');
111
112
  let systemPrompt = basePrompt;
112
113
  systemPrompt += LIVE_CONVERSATION_HINT;
114
+ // Pi has no native skill machinery (Claude's SDK and Codex discover skills
115
+ // themselves), so inject the name+description index here — the agent reads
116
+ // skills/<name>/SKILL.md on demand. Customer-facing supportPrompt runs skip
117
+ // this builder entirely, so they never see the index.
118
+ systemPrompt += buildSkillsIndex();
113
119
  systemPrompt += `\n\n---\n# Your Memory Files\n\n## MYSELF.md\n${memoryFiles.myself}\n\n## MYHUMAN.md\n${memoryFiles.myhuman}\n\n## MEMORY.md\n${memoryFiles.memory}\n\n---\n# Your Config Files\n\n## PULSE.json\n${memoryFiles.pulse}\n\n## CRONS.json\n${memoryFiles.crons}`;
114
120
 
115
121
  try {
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Shared skill plumbing for all three harnesses.
3
+ *
4
+ * Canonical on-disk layout (single source of truth):
5
+ * workspace/skills/<name>/SKILL.md — YAML frontmatter with `name` (= folder
6
+ * name) and `description`. See SKILL_FORMAT_MIGRATION.md.
7
+ *
8
+ * Each harness consumes that one layout its own way:
9
+ * - claude: mirrored into `workspace/.claude/skills` (the Agent SDK's
10
+ * project-skill discovery root) and enabled via the `skills` option —
11
+ * the SDK then lists name+description in context and lazy-loads bodies
12
+ * through its native Skill tool.
13
+ * - codex: mirrored into `workspace/.codex/skills` (codex's repo-scope
14
+ * root) and primed via `skills/list` — codex's own router takes over.
15
+ * - pi: no native skill machinery, so `buildSkillsIndex()` appends a
16
+ * name+description index to the system prompt and the agent reads
17
+ * `skills/<name>/SKILL.md` on demand (hermes-style progressive
18
+ * disclosure: metadata always in context, body only when used).
19
+ */
20
+
21
+ import fs from 'fs';
22
+ import path from 'path';
23
+ import { log } from '../../shared/logger.js';
24
+ import { WORKSPACE_DIR } from '../../shared/paths.js';
25
+
26
+ export const SKILLS_DIR = path.join(WORKSPACE_DIR, 'skills');
27
+
28
+ /** Sorted names of installed skill folders (dirs or symlinks under skills/). */
29
+ export function listSkillNames(): string[] {
30
+ try {
31
+ return fs.readdirSync(SKILLS_DIR, { withFileTypes: true })
32
+ .filter((e) => (e.isDirectory() || e.isSymbolicLink()) && !e.name.startsWith('.'))
33
+ .map((e) => e.name)
34
+ .sort();
35
+ } catch {
36
+ return []; // no skills dir — nothing installed
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Mirror workspace/skills/<name> → <mirrorRoot>/<name> as symlinks
42
+ * (idempotent), and prune symlinks for skills that were uninstalled.
43
+ * Returns the sorted list of mirrored skill names.
44
+ *
45
+ * Only symlinks are ever pruned — a real directory someone dropped into the
46
+ * mirror root is left alone.
47
+ */
48
+ export function mirrorSkillsInto(mirrorRoot: string, label: string): string[] {
49
+ const names = listSkillNames();
50
+ if (names.length) {
51
+ try { fs.mkdirSync(mirrorRoot, { recursive: true }); } catch {}
52
+ }
53
+
54
+ const mirrored: string[] = [];
55
+ for (const name of names) {
56
+ const target = path.join(SKILLS_DIR, name);
57
+ const link = path.join(mirrorRoot, name);
58
+ try {
59
+ const cur = fs.existsSync(link) ? fs.realpathSync(link) : null;
60
+ if (cur !== fs.realpathSync(target)) {
61
+ try { fs.rmSync(link, { recursive: true, force: true }); } catch {}
62
+ fs.symlinkSync(target, link, 'dir');
63
+ }
64
+ mirrored.push(name);
65
+ } catch (err: any) {
66
+ log.warn(`[${label}] could not mirror skill "${name}" into ${path.basename(path.dirname(mirrorRoot))}/skills: ${err.message}`);
67
+ }
68
+ }
69
+
70
+ // Prune stale symlinks (skill uninstalled) so dead links never reach the harness.
71
+ try {
72
+ for (const entry of fs.readdirSync(mirrorRoot, { withFileTypes: true })) {
73
+ if (!entry.isSymbolicLink() || names.includes(entry.name)) continue;
74
+ try { fs.unlinkSync(path.join(mirrorRoot, entry.name)); } catch {}
75
+ }
76
+ } catch {}
77
+
78
+ return mirrored;
79
+ }
80
+
81
+ /**
82
+ * Parse `name` and `description` from a SKILL.md YAML frontmatter block.
83
+ * Handles plain, quoted, and folded/literal (`>-`, `|`) scalar styles —
84
+ * enough for the two mandated keys without pulling in a YAML dependency.
85
+ */
86
+ export function parseSkillFrontmatter(skillMdPath: string): { name?: string; description?: string } {
87
+ let raw: string;
88
+ try {
89
+ raw = fs.readFileSync(skillMdPath, 'utf-8');
90
+ } catch {
91
+ return {};
92
+ }
93
+ if (!raw.startsWith('---')) return {};
94
+ const end = raw.indexOf('\n---', 3);
95
+ if (end === -1) return {};
96
+ const lines = raw.slice(raw.indexOf('\n') + 1, end).split('\n');
97
+
98
+ const out: Record<string, string> = {};
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const m = lines[i].match(/^(name|description):\s*(.*)$/);
101
+ if (!m) continue;
102
+ let value = m[2].trim();
103
+ if (/^[>|][+-]?$/.test(value)) {
104
+ // Block scalar — collect indented continuation lines, fold with spaces.
105
+ const parts: string[] = [];
106
+ while (i + 1 < lines.length && (/^\s+\S/.test(lines[i + 1]) || lines[i + 1].trim() === '')) {
107
+ i++;
108
+ if (lines[i].trim()) parts.push(lines[i].trim());
109
+ }
110
+ value = parts.join(' ');
111
+ } else if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
112
+ value = value.slice(1, -1).replace(/\\"/g, '"');
113
+ }
114
+ out[m[1]] = value;
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Compact installed-skills index for system-prompt injection (pi harness).
121
+ * One name+description line per skill — the body stays on disk until the
122
+ * agent actually opens it. Returns '' when no skills are installed.
123
+ */
124
+ export function buildSkillsIndex(): string {
125
+ const entries: string[] = [];
126
+ for (const name of listSkillNames()) {
127
+ const fm = parseSkillFrontmatter(path.join(SKILLS_DIR, name, 'SKILL.md'));
128
+ const description = fm.description || '(no description — open the SKILL.md)';
129
+ entries.push(`- **${fm.name || name}** — ${description}`);
130
+ }
131
+ if (!entries.length) return '';
132
+ return `\n\n---\n# Installed Skills\n\nScan this list on every request. When a request matches a skill — even partially — read \`skills/<name>/SKILL.md\` before acting and follow it.\n\n${entries.join('\n')}`;
133
+ }
@@ -194,7 +194,7 @@ self.addEventListener('fetch', function(event) {
194
194
  if (HASHED_RE.test(url.pathname)) {
195
195
  event.respondWith(caches.open(CACHE).then(function(c) {
196
196
  return c.match(request).then(function(hit) {
197
- return hit || fetch(request).then(function(r) { if (r.ok) c.put(request, r.clone()); return r; });
197
+ return hit || fetch(request).then(function(r) { if (r.status === 200) c.put(request, r.clone()); return r; });
198
198
  });
199
199
  }));
200
200
  return;
@@ -209,7 +209,7 @@ self.addEventListener('fetch', function(event) {
209
209
  return c.match(request).then(function(hit) {
210
210
  if (hit) return hit;
211
211
  return fetch(request).then(function(r) {
212
- if (r.ok) {
212
+ if (r.status === 200) { // never 206 — Cache.put rejects partial (Range) responses
213
213
  c.put(request, r.clone());
214
214
  c.keys().then(function(keys) {
215
215
  for (var i = 0; i < keys.length; i++) {
@@ -237,7 +237,7 @@ self.addEventListener('fetch', function(event) {
237
237
  // Network-first: always fetch the live dashboard; only fall back to cache when offline.
238
238
  event.respondWith(caches.open(CACHE).then(function(c) {
239
239
  return fetch(request)
240
- .then(function(r) { if (r.ok) c.put('/', r.clone()); return r; })
240
+ .then(function(r) { if (r.status === 200) c.put('/', r.clone()); return r; })
241
241
  .catch(function() { return c.match('/'); });
242
242
  }));
243
243
  return;
@@ -249,7 +249,7 @@ self.addEventListener('fetch', function(event) {
249
249
  // Everything else (JS/CSS modules, static assets) → network-first, cache as offline fallback.
250
250
  event.respondWith(caches.open(CACHE).then(function(c) {
251
251
  return fetch(request)
252
- .then(function(r) { if (r.ok) c.put(request, r.clone()); return r; })
252
+ .then(function(r) { if (r.status === 200) c.put(request, r.clone()); return r; })
253
253
  .catch(function() { return c.match(request); });
254
254
  }));
255
255
  });
@@ -724,6 +724,12 @@ export async function startSupervisor() {
724
724
 
725
725
  // HTTP request handler — proxies to Vite dev servers + worker API
726
726
  server.on('request', async (req, res) => {
727
+ // Every response carries the agent-origin stamp. The relay treats it as authoritative
728
+ // proof the bytes came from the agent (never substitutes a branded page) AND skips its
729
+ // 4 KB/1.5 s error-body sniff buffer on 4xx/5xx — without this, every legit 404/500
730
+ // through the tunnel paid the sniff. Proxied responses keep it unless upstream overrides.
731
+ res.setHeader('X-Bloby-Origin', 'supervisor');
732
+
727
733
  // Request timing sample — attached before any routing so every branch is covered.
728
734
  {
729
735
  const t0 = Date.now();
@@ -77,18 +77,19 @@ export async function startViteDevServers(supervisorPort: number, hmrServer: htt
77
77
 
78
78
  log.ok(`Vite HMR active — dashboard :${ports.dashboard}`);
79
79
 
80
- // Warm up: fetch the dashboard entry to pre-transform modules
81
- fetch(`http://127.0.0.1:${ports.dashboard}/`).then(r => r.text()).then(async (html) => {
82
- const scriptRe = /src="([^"]+\.tsx)"/g;
83
- let m;
84
- while ((m = scriptRe.exec(html)) !== null) {
85
- const url = `http://127.0.0.1:${ports.dashboard}${m[1].startsWith('/') ? '' : '/'}${m[1]}`;
86
- await fetch(url).then(r => r.text()).catch(() => {});
87
- }
88
- console.log('__VITE_WARM__');
89
- }).catch(() => {
90
- console.log('__VITE_WARM__');
91
- });
80
+ // Warm up: one fetch of the entry HTML (html transform isn't covered by server.warmup),
81
+ // then wait for the warmup graph (vite.config.ts now lists the whole src tree) to finish
82
+ // transforming. __VITE_WARM__ (consumed by the CLI spinner) used to print after only
83
+ // main.tsx — "warm" now actually means warm. Timeout guard so a huge graph or a wedged
84
+ // transform can never hang the boot signal.
85
+ const warm = dashboardVite;
86
+ Promise.resolve()
87
+ .then(() => fetch(`http://127.0.0.1:${ports.dashboard}/`).then((r) => r.text()).catch(() => {}))
88
+ .then(() => Promise.race([
89
+ warm ? warm.waitForRequestsIdle() : Promise.resolve(),
90
+ new Promise((resolve) => setTimeout(resolve, 20_000)),
91
+ ]))
92
+ .finally(() => console.log('__VITE_WARM__'));
92
93
 
93
94
  return ports;
94
95
  }
package/vite.config.ts CHANGED
@@ -75,7 +75,10 @@ export default defineConfig({
75
75
  '/api': 'http://localhost:7400',
76
76
  },
77
77
  warmup: {
78
- clientFiles: ['./src/main.tsx'],
78
+ // The whole client graph, not just the entry — the old single-file warmup only
79
+ // pre-transformed main.tsx, so the first browser hit still paid transform time for
80
+ // every other module (felt hardest on Pi-class hardware after a restart).
81
+ clientFiles: ['./src/main.tsx', './src/**/*.tsx', './src/**/*.ts', './src/**/*.css'],
79
82
  },
80
83
  watch: {
81
84
  ignored: [
@@ -11,7 +11,12 @@
11
11
  <link rel="icon" type="image/png" href="/morphy-favicon.png" />
12
12
  <link rel="apple-touch-icon" href="/morphy-icon-192.png" />
13
13
  <link rel="manifest" href="/manifest.json" />
14
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
14
+ <!-- Fonts load without blocking first paint: the stylesheet applies onload (the
15
+ media="print" trick), display=swap keeps text visible meanwhile. -->
16
+ <link rel="preconnect" href="https://fonts.googleapis.com">
17
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
19
+ <noscript><link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet"></noscript>
15
20
  <title>Bloby - AI agent with its own workspace</title>
16
21
  </head>
17
22
  <body class="bg-background text-foreground" style="background-color:#0A0A0A">
@@ -57,7 +57,7 @@ self.addEventListener('fetch', (event) => {
57
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
- hit || fetch(request).then(r => { if (r.ok) c.put(request, r.clone()); return r; })
60
+ hit || fetch(request).then(r => { if (r.status === 200) c.put(request, r.clone()); return r; })
61
61
  )
62
62
  ));
63
63
  return;
@@ -70,7 +70,7 @@ self.addEventListener('fetch', (event) => {
70
70
  event.respondWith(caches.open(CACHE).then(c =>
71
71
  c.match(request).then(hit =>
72
72
  hit || fetch(request).then(r => {
73
- if (r.ok) {
73
+ if (r.status === 200) { // never 206 — Cache.put rejects partial (Range) responses
74
74
  c.put(request, r.clone());
75
75
  c.keys().then(keys => {
76
76
  for (const k of keys) {
@@ -94,7 +94,7 @@ self.addEventListener('fetch', (event) => {
94
94
  // Network-first: always fetch the live dashboard; cache only as offline fallback.
95
95
  event.respondWith(caches.open(CACHE).then(c =>
96
96
  fetch(request)
97
- .then(r => { if (r.ok) c.put('/', r.clone()); return r; })
97
+ .then(r => { if (r.status === 200) c.put('/', r.clone()); return r; })
98
98
  .catch(() => c.match('/'))
99
99
  ));
100
100
  return;
@@ -107,7 +107,7 @@ self.addEventListener('fetch', (event) => {
107
107
  // Covers: JS/CSS modules, images, video, fonts, manifest, etc.
108
108
  event.respondWith(caches.open(CACHE).then(c =>
109
109
  fetch(request)
110
- .then(r => { if (r.ok) c.put(request, r.clone()); return r; })
110
+ .then(r => { if (r.status === 200) c.put(request, r.clone()); return r; })
111
111
  .catch(() => c.match(request))
112
112
  ));
113
113
  });
@@ -5,11 +5,24 @@
5
5
  "bloby_human": "Bruno Bertapeli",
6
6
  "bloby": "bloby-bruno",
7
7
  "author": "newbot-official",
8
- "description": "Morphy native macOS companion. Activates on the [Mac] tag. You reply with a concise spoken line (TTS) and optionally drive the Mac's action registry — one <mac_actions> JSON array that can show a notch card (preset), point the mascot at the screen, or spotlight a control. Custom cards use raw <notch_html>. The same registry works proactively (PULSE/cron) wrapped in <mac_push>. Card presets + schemas: presets/PRESETS.md. Reusable custom cards: frequentSnippets/.",
8
+ "description": "Morphy native macOS companion. Activates on the [Mac] tag. You reply with a concise spoken line (TTS) and optionally drive the Mac's action registry — one <mac_actions> JSON array that can show a notch card, point the mascot at the screen, or spotlight a control. Custom cards use <notch_html>. The same registry works proactively (PULSE/cron) wrapped in <mac_push>. Card presets + schemas: presets/PRESETS.md. Reusable custom cards: frequentSnippets/.",
9
9
  "depends": [],
10
10
  "env_keys": [],
11
11
  "has_telemetry": false,
12
12
  "size": "12KB",
13
13
  "contains_binaries": false,
14
- "tags": ["mac", "morphy", "notch", "macos", "voice", "tts", "visual", "html", "registry", "actions", "spotlight", "point"]
14
+ "tags": [
15
+ "mac",
16
+ "morphy",
17
+ "notch",
18
+ "macos",
19
+ "voice",
20
+ "tts",
21
+ "visual",
22
+ "html",
23
+ "registry",
24
+ "actions",
25
+ "spotlight",
26
+ "point"
27
+ ]
15
28
  }
@@ -1,6 +0,0 @@
1
- {
2
- "name": "telegram",
3
- "version": "1.0.0",
4
- "description": "Telegram channel via your own @BotFather bot token. Paste-token connect, direct long-poll, voice/photo, channel/business/assistant modes.",
5
- "skills": "./"
6
- }
@@ -1,6 +0,0 @@
1
- {
2
- "name": "whatsapp",
3
- "version": "2.0.0",
4
- "description": "WhatsApp channel via Baileys. QR auth, messaging, voice transcription, channel and business modes.",
5
- "skills": "./"
6
- }