bloby-bot 0.69.6 → 0.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/package.json +1 -1
- package/supervisor/chat/bloby-main.tsx +1 -1
- package/supervisor/harnesses/claude.ts +24 -0
- package/supervisor/harnesses/codex.ts +2 -27
- package/supervisor/harnesses/pi/index.ts +6 -0
- package/supervisor/harnesses/skills.ts +133 -0
- package/supervisor/index.ts +6 -0
- package/supervisor/vite-dev.ts +13 -12
- package/vite.config.ts +4 -1
- package/workspace/client/index.html +6 -1
- package/workspace/skills/mac/skill.json +15 -2
- package/workspace/skills/telegram/.claude-plugin/plugin.json +0 -6
- package/workspace/skills/whatsapp/.claude-plugin/plugin.json +0 -6
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/
|
|
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
|
|
196
|
+
## Skills
|
|
197
197
|
|
|
198
|
-
The agent auto-discovers skills in `workspace/skills/`. Each skill is a folder containing `.claude
|
|
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/
|
|
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
|
@@ -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 ? '
|
|
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
|
-
|
|
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
|
+
}
|
package/supervisor/index.ts
CHANGED
|
@@ -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();
|
package/supervisor/vite-dev.ts
CHANGED
|
@@ -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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
@@ -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
|
|
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": [
|
|
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
|
}
|