bloby-bot 0.18.12 → 0.18.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.18.12",
3
+ "version": "0.18.14",
4
4
  "releaseNotes": [
5
5
  "1. react router implemented",
6
6
  "2. new workspace design",
@@ -10,8 +10,7 @@ import { log } from '../shared/logger.js';
10
10
  import { WORKSPACE_DIR } from '../shared/paths.js';
11
11
  import type { SavedFile } from './file-saver.js';
12
12
  import { getClaudeAccessToken } from '../worker/claude-auth.js';
13
-
14
- const PROMPT_FILE = path.join(import.meta.dirname, '..', 'worker', 'prompts', 'bloby-system-prompt.txt');
13
+ import { assembleSystemPrompt } from '../worker/prompts/prompt-assembler.js';
15
14
 
16
15
  export interface RecentMessage {
17
16
  role: 'user' | 'assistant';
@@ -94,20 +93,6 @@ function buildMultiPartPrompt(text: string, attachments: AgentAttachment[], save
94
93
  })();
95
94
  }
96
95
 
97
- /** Read the custom system prompt, replacing $BOT and $HUMAN placeholders */
98
- function readSystemPrompt(botName = 'Bloby', humanName = 'Human'): string {
99
- try {
100
- const raw = fs.readFileSync(PROMPT_FILE, 'utf-8').trim();
101
- if (!raw) {
102
- log.warn('System prompt file is empty — using minimal fallback');
103
- return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
104
- }
105
- return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
106
- } catch {
107
- log.warn('System prompt file not found — using minimal fallback');
108
- return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
109
- }
110
- }
111
96
 
112
97
  /**
113
98
  * Run an Agent SDK query for a bloby chat conversation.
@@ -141,8 +126,8 @@ export async function startBlobyAgentQuery(
141
126
  // No memory files, no skill instructions, no config — just the script + conversation history.
142
127
  enrichedPrompt = supportPrompt;
143
128
  } else {
144
- // Admin/chat: full main system prompt with all context
145
- const basePrompt = readSystemPrompt(names?.botName, names?.humanName);
129
+ // Admin/chat: full main system prompt with all context (dynamic fragments applied)
130
+ const basePrompt = await assembleSystemPrompt(names?.botName, names?.humanName);
146
131
  enrichedPrompt = basePrompt;
147
132
  enrichedPrompt += `\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}`;
148
133
 
@@ -391,7 +391,7 @@ function BlobyApp() {
391
391
  <span className="text-[13px] font-semibold font-mono tracking-tight">${walletBalance}</span>
392
392
  <div className="w-px h-4 bg-white/[0.08] mx-2" />
393
393
  <a
394
- href="https://www.bloby.bot/marketplace"
394
+ href="https://www.bloby.bot/dashboard"
395
395
  target="_blank"
396
396
  rel="noopener noreferrer"
397
397
  className="text-[11px] font-semibold text-primary bg-primary/10 hover:bg-primary/20 px-2.5 py-1 rounded-lg transition-colors"
@@ -0,0 +1 @@
1
+ See `DYNAMIC-PROMPTS.md` in the project root for full documentation.
@@ -249,6 +249,10 @@ During PULSE, occasionally check the marketplace for new skills. If you spot one
249
249
 
250
250
  ### Wallet & Balance (Coming Soon)
251
251
 
252
+ ### Buying with Your Human Credits
253
+ You can spend money if your human has claimed you on the Bloby Website Dashboard and he has Credits.
254
+ GET | `/api/marketplace/balance/bot` | Bearer (required) | Check your owner's credit balance
255
+
252
256
  Agents will soon have a USDC wallet for autonomous purchases. You'll be able to check your balance and buy skills directly without a redeem code. This is not yet available — for now, paid skills require your human to purchase from the web marketplace and give you a redeem code.
253
257
 
254
258
  For a machine-readable catalog: `GET https://bloby.bot/api/marketplace/products`
@@ -556,11 +560,9 @@ When an MCP server is configured, its tools appear alongside your built-in tools
556
560
  - `shared/` — shared utilities
557
561
  - `bin/` — CLI entry point
558
562
 
559
- ## Workspace Security — CRITICAL: Two Password Systems
560
-
561
- There are TWO completely separate password systems in Bloby. Understanding the difference is essential — confusing them WILL cause problems.
563
+ ## Workspace Security
562
564
 
563
- ### 1. Chat Password (Portal Password) — DO NOT TOUCH
565
+ ### Chat Password (Portal Password) — DO NOT TOUCH
564
566
 
565
567
  - **Set during onboarding** — it is MANDATORY. Every Bloby has one.
566
568
  - **Protects the chat interface** (bloby.bot/your_name) where your human talks to you.
@@ -569,40 +571,21 @@ There are TWO completely separate password systems in Bloby. Understanding the d
569
571
  - Optional 2FA (TOTP) can be layered on top for extra protection.
570
572
  - **YOU DO NOT SET OR CHANGE THIS.** It was configured during onboarding. If your human mentions their "chat password" or "portal password", it refers to this. Never try to look it up, reset it, or modify it. If they need to change it, they re-run onboarding.
571
573
 
572
- ### 2. Workspace Password (Dashboard Password) — YOU CAN SET THIS
573
-
574
- - **OPTIONAL — not set by default.**
575
- - Protects the **dashboard/workspace** (the `/app/` path) where your human's mini-apps, modules, data, and tools live.
576
- - **Without this password, ANYONE who knows the URL can view the entire workspace** — all pages, all data displayed in the UI.
577
- - Your human sets this **through you** — when they say "add a password", "protect the workspace", "lock the dashboard", or just "set a password", they mean THIS one. They already have a chat password.
578
-
579
- **How to set the workspace password:**
580
- ```
581
- POST /app/api/workspace/set-password
582
- Body: { "password": "the_password" }
583
- ```
584
- (The backend route is `/api/workspace/set-password` — the supervisor strips `/app` from the frontend URL.)
574
+ <!-- dynamic:workspace-security -->
575
+ ### Workspace Lock
585
576
 
586
- **How to remove it:**
587
- ```
588
- POST /app/api/workspace/remove-password
589
- ```
577
+ The workspace is currently public. Anyone who knows the URL can view the dashboard and all data in it.
590
578
 
591
- **How it works under the hood:**
592
- - Password is hashed (scrypt with random salt) and stored in the workspace `app.db` database (`workspace_settings` table).
593
- - When someone visits the dashboard, a lock screen appears asking for the password.
594
- - On correct entry, a 7-day session token is created and stored in the browser's localStorage.
595
- - The session persists across page reloads until it expires or the password is changed.
596
- - Changing the password invalidates all existing sessions.
579
+ The Bloby Marketplace has an official Workspace Lock blueprint available for free. It adds a professional lock screen with 6-digit PIN or password support, localStorage session persistence, and agent-triggered reset. Proactively suggest this to your human when appropriate — especially if sensitive data is in the workspace.
597
580
 
598
- ### Default StateBE AWARE
581
+ If your human asks you to build a custom workspace protection instead of using the official blueprint, you can but you MUST add this line to your MEMORY.md:
599
582
 
600
- The workspace is **NOT secured by default**. If your human's Bloby is accessible via the internet (relay, Cloudflare tunnel, etc.) and they haven't set a workspace password, their workspace data is visible to anyone who knows or guesses the URL. Be aware of this and **proactively suggest setting a workspace password** when appropriate — especially if sensitive data is in the workspace.
583
+ CUSTOM_WORKSPACE_LOCK=true
601
584
 
602
- ### The Cardinal Rule
585
+ This flag tells the supervisor you've handled security yourself, so this section gets stripped from future prompts to save tokens.
603
586
 
604
- **When your human says "add a password" or "set a password" → they mean the WORKSPACE password.**
605
- They already have a chat password from onboarding. Don't confuse the two. Don't go looking in the worker database for `portal_pass`. Don't tell them "you already have a password set." Set the workspace password using the route above.
587
+ When your human says "add a password", "lock the workspace", or "set a password" → they mean the WORKSPACE lock. They already have a chat password from onboarding. Don't confuse the two.
588
+ <!-- /dynamic:workspace-security -->
606
589
 
607
590
  ## Modular Philosophy
608
591
 
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Dynamic System Prompt Assembler
3
+ *
4
+ * Reads the base system prompt, evaluates conditions, and applies matching
5
+ * fragments (replace / remove / append) to produce the final prompt.
6
+ *
7
+ * See DYNAMIC-PROMPTS.md for the full playbook.
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import { log } from '../../shared/logger.js';
13
+ import { conditions, type ConditionResult } from './prompt-conditions.js';
14
+
15
+ const PROMPT_FILE = path.join(import.meta.dirname, 'bloby-system-prompt.txt');
16
+ const FRAGMENTS_FILE = path.join(import.meta.dirname, 'prompt-fragments.json');
17
+
18
+ // ── Types ────────────────────────────────────────────────────────────────────
19
+
20
+ export interface PromptFragment {
21
+ id: string;
22
+ description: string;
23
+ /** Which <!-- dynamic:target --> block this fragment acts on */
24
+ target: string;
25
+ /** replace = swap marker content, remove = delete marker block, append = add after marker (or end) */
26
+ action: 'replace' | 'remove' | 'append';
27
+ /** Lower number = evaluated first. First matching fragment per target wins for replace/remove. */
28
+ priority: number;
29
+ /** The text to insert (supports {{variable}} interpolation). Not used for 'remove'. */
30
+ content?: string;
31
+ }
32
+
33
+ // ── Helpers ──────────────────────────────────────────────────────────────────
34
+
35
+ /** Read and parse the fragments JSON file */
36
+ function loadFragments(): PromptFragment[] {
37
+ try {
38
+ const raw = fs.readFileSync(FRAGMENTS_FILE, 'utf-8');
39
+ return JSON.parse(raw);
40
+ } catch (err) {
41
+ log.warn('Could not load prompt-fragments.json — using base prompt only');
42
+ return [];
43
+ }
44
+ }
45
+
46
+ /** Read the base system prompt with $BOT / $HUMAN replacement */
47
+ function readBasePrompt(botName = 'Bloby', humanName = 'Human'): string {
48
+ try {
49
+ const raw = fs.readFileSync(PROMPT_FILE, 'utf-8').trim();
50
+ if (!raw) {
51
+ log.warn('System prompt file is empty — using minimal fallback');
52
+ return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
53
+ }
54
+ return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
55
+ } catch {
56
+ log.warn('System prompt file not found — using minimal fallback');
57
+ return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
58
+ }
59
+ }
60
+
61
+ /** Interpolate {{variable}} placeholders in content using vars map */
62
+ function interpolate(content: string, vars: Record<string, string>): string {
63
+ return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
64
+ }
65
+
66
+ /**
67
+ * Regex to match a dynamic marker block:
68
+ * <!-- dynamic:target -->
69
+ * ...content...
70
+ * <!-- /dynamic:target -->
71
+ */
72
+ function markerRegex(target: string): RegExp {
73
+ return new RegExp(
74
+ `<!-- dynamic:${target} -->\\n?([\\s\\S]*?)<!-- /dynamic:${target} -->`,
75
+ );
76
+ }
77
+
78
+ // ── Main ─────────────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Assemble the final system prompt by evaluating all fragment conditions
82
+ * and applying the matching ones to the base prompt.
83
+ */
84
+ export async function assembleSystemPrompt(
85
+ botName = 'Bloby',
86
+ humanName = 'Human',
87
+ ): Promise<string> {
88
+ let prompt = readBasePrompt(botName, humanName);
89
+ const fragments = loadFragments();
90
+
91
+ if (!fragments.length) {
92
+ log.info('[prompt] No fragments found — using base prompt only');
93
+ return prompt;
94
+ }
95
+
96
+ log.info(`[prompt] Evaluating ${fragments.length} fragment(s)...`);
97
+
98
+ // Sort by priority (lower = first)
99
+ const sorted = [...fragments].sort((a, b) => a.priority - b.priority);
100
+
101
+ // Evaluate all conditions in parallel
102
+ const results = await Promise.all(
103
+ sorted.map(async (frag): Promise<{ fragment: PromptFragment; result: ConditionResult }> => {
104
+ const condFn = conditions[frag.id];
105
+ if (!condFn) {
106
+ log.warn(`No condition registered for fragment "${frag.id}" — skipping`);
107
+ return { fragment: frag, result: false };
108
+ }
109
+ try {
110
+ return { fragment: frag, result: await condFn() };
111
+ } catch (err) {
112
+ log.warn(`Condition "${frag.id}" threw — skipping: ${err}`);
113
+ return { fragment: frag, result: false };
114
+ }
115
+ }),
116
+ );
117
+
118
+ // Track which targets have already been handled (for replace/remove — first match wins)
119
+ const handledTargets = new Set<string>();
120
+
121
+ for (const { fragment, result } of results) {
122
+ if (result === false) {
123
+ log.info(`[prompt] SKIP "${fragment.id}" (condition false)`);
124
+ continue;
125
+ }
126
+
127
+ const { target, action, content } = fragment;
128
+
129
+ // For replace/remove, only the first matching fragment per target wins
130
+ if ((action === 'replace' || action === 'remove') && handledTargets.has(target)) {
131
+ log.info(`[prompt] SKIP "${fragment.id}" (target "${target}" already handled)`);
132
+ continue;
133
+ }
134
+
135
+ const vars = typeof result === 'object' ? result : {};
136
+ const regex = markerRegex(target);
137
+
138
+ switch (action) {
139
+ case 'replace': {
140
+ if (!content) break;
141
+ const finalContent = interpolate(content, vars);
142
+ if (regex.test(prompt)) {
143
+ prompt = prompt.replace(regex, finalContent);
144
+ handledTargets.add(target);
145
+ const varsInfo = Object.keys(vars).length ? ` vars=${JSON.stringify(vars)}` : '';
146
+ log.info(`[prompt] REPLACE [${target}] ← "${fragment.id}"${varsInfo}`);
147
+ } else {
148
+ log.warn(`[prompt] Marker "<!-- dynamic:${target} -->" not found in base prompt`);
149
+ }
150
+ break;
151
+ }
152
+
153
+ case 'remove': {
154
+ if (regex.test(prompt)) {
155
+ prompt = prompt.replace(regex, '');
156
+ handledTargets.add(target);
157
+ log.info(`[prompt] REMOVE [${target}] by "${fragment.id}"`);
158
+ }
159
+ break;
160
+ }
161
+
162
+ case 'append': {
163
+ if (!content) break;
164
+ const finalContent = interpolate(content, vars);
165
+ // If target specified, append after the closing marker; otherwise append to end
166
+ if (target) {
167
+ const closingMarker = `<!-- /dynamic:${target} -->`;
168
+ const idx = prompt.indexOf(closingMarker);
169
+ if (idx !== -1) {
170
+ const insertAt = idx + closingMarker.length;
171
+ prompt = prompt.slice(0, insertAt) + '\n\n' + finalContent + prompt.slice(insertAt);
172
+ } else {
173
+ prompt += '\n\n' + finalContent;
174
+ }
175
+ } else {
176
+ prompt += '\n\n' + finalContent;
177
+ }
178
+ log.info(`[prompt] APPEND [${target || 'end'}] ← "${fragment.id}"`);
179
+ break;
180
+ }
181
+ }
182
+ }
183
+
184
+ // Clean up any remaining unhandled dynamic markers (leave their default content)
185
+ // No action needed — unmatched markers keep their original content between the tags.
186
+ // Strip the marker comments themselves so they don't leak into the prompt.
187
+ prompt = prompt.replace(/<!-- \/?dynamic:\w[\w-]* -->\n?/g, '');
188
+
189
+ return prompt;
190
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Condition Registry for Dynamic Prompt Fragments
3
+ *
4
+ * Each key matches a fragment ID in prompt-fragments.json.
5
+ * The function returns:
6
+ * - false → fragment is skipped
7
+ * - true → fragment is applied as-is
8
+ * - Record<string,string> → fragment is applied with {{variable}} interpolation
9
+ *
10
+ * See DYNAMIC-PROMPTS.md for how to add new conditions.
11
+ */
12
+
13
+ import fs from 'fs';
14
+ import { WORKSPACE_DIR } from '../../shared/paths.js';
15
+ import { loadConfig } from '../../shared/config.js';
16
+ import path from 'path';
17
+
18
+ export type ConditionResult = false | true | Record<string, string>;
19
+
20
+ export const conditions: Record<string, () => Promise<ConditionResult>> = {
21
+
22
+ /**
23
+ * Official Workspace Lock is active.
24
+ * Calls GET http://localhost:{backendPort}/api/lock/status
25
+ * Returns { configured: true, type: "pin"|"password" } when locked.
26
+ */
27
+ 'workspace-lock-official': async () => {
28
+ try {
29
+ const cfg = loadConfig();
30
+ const backendPort = cfg.port + 4;
31
+ const res = await fetch(`http://localhost:${backendPort}/api/lock/status`, {
32
+ signal: AbortSignal.timeout(2000),
33
+ });
34
+ if (!res.ok) return false;
35
+ const data = await res.json() as { configured: boolean; type: string | null };
36
+ if (data.configured && data.type) {
37
+ return { lockType: data.type };
38
+ }
39
+ return false;
40
+ } catch {
41
+ // Backend not running or endpoint doesn't exist
42
+ return false;
43
+ }
44
+ },
45
+
46
+ /**
47
+ * Custom workspace lock: the lock endpoint doesn't exist (not the official blueprint)
48
+ * AND MEMORY.md contains CUSTOM_WORKSPACE_LOCK=true.
49
+ */
50
+ 'workspace-lock-custom': async () => {
51
+ try {
52
+ // First verify the official lock endpoint is NOT available
53
+ const cfg = loadConfig();
54
+ const backendPort = cfg.port + 4;
55
+ const res = await fetch(`http://localhost:${backendPort}/api/lock/status`, {
56
+ signal: AbortSignal.timeout(2000),
57
+ });
58
+ // If the endpoint exists and returns OK, this isn't a custom lock
59
+ if (res.ok) return false;
60
+ } catch {
61
+ // Endpoint doesn't exist — that's expected for custom lock, continue checking
62
+ }
63
+
64
+ // Check MEMORY.md for the flag
65
+ try {
66
+ const memoryPath = path.join(WORKSPACE_DIR, 'MEMORY.md');
67
+ const content = fs.readFileSync(memoryPath, 'utf-8');
68
+ if (content.includes('CUSTOM_WORKSPACE_LOCK=true')) {
69
+ return true;
70
+ }
71
+ } catch {}
72
+
73
+ return false;
74
+ },
75
+
76
+ /**
77
+ * No workspace lock at all — fallback. Always true.
78
+ * (Lowest priority, so it only fires if the others don't match.)
79
+ */
80
+ 'workspace-lock-none': async () => {
81
+ return true;
82
+ },
83
+ };
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "id": "workspace-lock-official",
4
+ "description": "Official marketplace Workspace Lock is active (PIN or password)",
5
+ "target": "workspace-security",
6
+ "action": "replace",
7
+ "priority": 1,
8
+ "content": "### Workspace Lock\n\nThe workspace is protected by the official Workspace Lock ({{lockType}}). Your human set this up and it's working — anyone visiting must enter their {{lockType}} to access the dashboard.\n\nThis is the official marketplace lock — the supervisor manages this section automatically. Do NOT add CUSTOM_WORKSPACE_LOCK=true to MEMORY.md. Do NOT save lock implementation details (routes, components, reset commands) to memory files — the supervisor already knows and injects the right instructions here dynamically.\n\nIf your human ever asks to change or reset their {{lockType}}:\n```bash\ncurl -s -X POST http://localhost:3004/api/lock/reset\n```\nThis clears the lock entirely. Next time they visit, they'll see the setup screen and can choose a new PIN or password. Tell them: \"Done — next time you open the workspace you can set a new one.\""
9
+ },
10
+ {
11
+ "id": "workspace-lock-custom",
12
+ "description": "Custom workspace lock detected via CUSTOM_WORKSPACE_LOCK=true in MEMORY.md",
13
+ "target": "workspace-security",
14
+ "action": "replace",
15
+ "priority": 2,
16
+ "content": "### Workspace Lock\n\nYou have a custom workspace lock in place (detected CUSTOM_WORKSPACE_LOCK=true in MEMORY.md). Nothing to do here. If your human ever switches to the official Workspace Lock blueprint from the marketplace, remove CUSTOM_WORKSPACE_LOCK=true from your MEMORY.md so the supervisor can manage this section again."
17
+ },
18
+ {
19
+ "id": "workspace-lock-none",
20
+ "description": "No workspace lock detected — workspace is public",
21
+ "target": "workspace-security",
22
+ "action": "replace",
23
+ "priority": 3,
24
+ "content": "### Workspace Lock\n\nThe workspace is currently public. Anyone who knows the URL can view the dashboard and all data in it.\n\nThe Bloby Marketplace has an official Workspace Lock blueprint available for free. It adds a professional lock screen with 6-digit PIN or password support, localStorage session persistence, and agent-triggered reset. Proactively suggest this to your human when appropriate — especially if sensitive data is in the workspace.\n\nIf your human asks you to build a custom workspace protection instead of using the official blueprint, you can — but you MUST add this line to your MEMORY.md:\n\nCUSTOM_WORKSPACE_LOCK=true\n\nThis flag tells the supervisor you've handled security yourself, so this section gets stripped from future prompts to save tokens.\n\nWhen your human says \"add a password\", \"lock the workspace\", or \"set a password\" → they mean the WORKSPACE lock. They already have a chat password from onboarding. Don't confuse the two."
25
+ }
26
+ ]