bloby-bot 0.21.13 → 0.22.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/bin/cli.js CHANGED
@@ -431,7 +431,7 @@ async function runNamedTunnelSetup() {
431
431
 
432
432
  // Generate cloudflared config
433
433
  const config = fs.existsSync(CONFIG_PATH) ? JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) : {};
434
- const port = config.port || 3000;
434
+ const port = config.port || 7400;
435
435
  const cfHome = path.join(os.homedir(), '.cloudflared');
436
436
  const cfConfigPath = path.join(DATA_DIR, 'cloudflared-config.yml');
437
437
 
@@ -720,7 +720,7 @@ function createConfig() {
720
720
  fs.mkdirSync(DATA_DIR, { recursive: true });
721
721
  if (!fs.existsSync(CONFIG_PATH)) {
722
722
  const config = {
723
- port: 3000,
723
+ port: 7400,
724
724
  username: '',
725
725
  ai: { provider: '', model: '', apiKey: '' },
726
726
  tunnel: { mode: 'quick' },
@@ -64,7 +64,7 @@ export function registerStartCommand(program: Command) {
64
64
  s.stop(pc.green('Server running'));
65
65
 
66
66
  console.log(`\n${pc.bold('Bloby is ready!')}`);
67
- console.log(` ${pc.dim('Local:')} ${pc.blue(`http://localhost:${config.port || 3000}`)}`);
67
+ console.log(` ${pc.dim('Local:')} ${pc.blue(`http://localhost:${config.port || 7400}`)}`);
68
68
 
69
69
  if (result.tunnelUrl && hasTunnel) {
70
70
  console.log(` ${pc.dim('Tunnel:')} ${pc.blue(result.tunnelUrl)}`);
@@ -54,7 +54,7 @@ async function runNamedTunnelSetup() {
54
54
  const domain = await text({ message: 'Your domain (e.g. bot.mydomain.com):' });
55
55
  if (isCancel(domain) || !domain) { cancel('Domain is required.'); process.exit(1); }
56
56
 
57
- const { port = 3000 } = safeLoadConfig();
57
+ const { port = 7400 } = safeLoadConfig();
58
58
  const cfHome = path.join(os.homedir(), '.cloudflared');
59
59
  const cfConfigPath = path.join(DATA_DIR, 'cloudflared-config.yml');
60
60
 
@@ -39,7 +39,7 @@ export function createConfig() {
39
39
  if (fs.existsSync(CONFIG_PATH)) return;
40
40
 
41
41
  const config: Partial<BotConfig> = {
42
- port: 3000,
42
+ port: 7400,
43
43
  username: '',
44
44
  ai: { provider: '', model: '', apiKey: '' },
45
45
  tunnel: { mode: 'quick' },
@@ -49,7 +49,7 @@ export function bootServer({
49
49
 
50
50
  resolve({
51
51
  child,
52
- tunnelUrl: tunnelUrl || `http://localhost:${config.port || 3000}`,
52
+ tunnelUrl: tunnelUrl || `http://localhost:${config.port || 7400}`,
53
53
  relayUrl: relayUrl || config.relay?.url || null,
54
54
  tunnelFailed,
55
55
  viteWarm,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.21.13",
3
+ "version": "0.22.1",
4
4
  "releaseNotes": [
5
5
  "1. react router implemented",
6
6
  "2. new workspace design",
package/shared/config.ts CHANGED
@@ -42,7 +42,7 @@ export interface BotConfig {
42
42
  }
43
43
 
44
44
  const DEFAULTS: BotConfig = {
45
- port: 3000,
45
+ port: 7400,
46
46
  username: '',
47
47
  ai: { provider: '', model: '', apiKey: '' },
48
48
  tunnel: { mode: 'quick' },
@@ -68,7 +68,7 @@ Your working directory is the `workspace/` folder. This is your full-stack works
68
68
 
69
69
  ## Backend Routing (Critical)
70
70
 
71
- A supervisor process sits in front of everything on port 3000. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
71
+ A supervisor process sits in front of everything on port 7400. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
72
72
 
73
73
  ```
74
74
  Browser: GET /app/api/tasks → Supervisor strips /app → Backend receives: GET /api/tasks
@@ -29,8 +29,8 @@ function formatTime(iso: string): string {
29
29
  /** Convert backtick-wrapped WhatsApp QR URL into a markdown link */
30
30
  function preprocessContent(text: string): string {
31
31
  return text.replace(
32
- /`http:\/\/localhost:3000\/api\/channels\/whatsapp\/qr-page`/g,
33
- '[pair-whatsapp](http://localhost:3000/api/channels/whatsapp/qr-page)'
32
+ /`http:\/\/localhost:\d+\/api\/channels\/whatsapp\/qr-page`/g,
33
+ '[pair-whatsapp](/api/channels/whatsapp/qr-page)'
34
34
  );
35
35
  }
36
36
 
@@ -21,7 +21,7 @@ export class WsClient {
21
21
 
22
22
  constructor(url?: string, tokenGetter?: (() => string | null) | null) {
23
23
  const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
24
- const host = import.meta.env.DEV ? 'localhost:3000' : location.host;
24
+ const host = import.meta.env.DEV ? 'localhost:7400' : location.host;
25
25
  this.url = url ?? `${proto}//${host}/ws`;
26
26
  this.tokenGetter = tokenGetter ?? null;
27
27
  }
@@ -215,11 +215,35 @@ const RECOVERING_HTML = `<!DOCTYPE html><html style="background:#222122"><head><
215
215
  </div><script>setTimeout(function(){location.reload()},3000)</script>
216
216
  <script src="/bloby/widget.js"></script></body></html>`;
217
217
 
218
+ /** Kill any stale process holding a port. Ensures clean startup after crashes/updates. */
219
+ function killPort(port: number): void {
220
+ try {
221
+ const pids = execSync(`lsof -ti :${port} 2>/dev/null`, { encoding: 'utf-8' }).trim();
222
+ if (pids) {
223
+ const pidList = pids.split('\n').filter(Boolean);
224
+ const ownPid = process.pid.toString();
225
+ const toKill = pidList.filter((p) => p !== ownPid);
226
+ if (toKill.length) {
227
+ log.info(`[startup] Killing stale process(es) on port ${port}: ${toKill.join(', ')}`);
228
+ execSync(`kill -9 ${toKill.join(' ')} 2>/dev/null`);
229
+ }
230
+ }
231
+ } catch {
232
+ // No process on port, or kill failed (already dead) — fine
233
+ }
234
+ }
235
+
218
236
  export async function startSupervisor() {
219
237
  const config = loadConfig();
220
238
  const backendPort = getBackendPort(config.port);
221
239
  const internalSecret = crypto.randomBytes(16).toString('hex');
222
240
 
241
+ // Kill any stale processes from previous crashes/updates
242
+ log.info(`[startup] Clearing ports ${config.port}, ${config.port + 2}, ${backendPort}...`);
243
+ killPort(config.port); // supervisor
244
+ killPort(config.port + 2); // vite
245
+ killPort(backendPort); // backend
246
+
223
247
  // Create HTTP server first (Vite needs it for HMR WebSocket)
224
248
  // The request handler is set up later via server.on('request')
225
249
  const server = http.createServer();
package/vite.config.ts CHANGED
@@ -24,10 +24,10 @@ export default defineConfig({
24
24
  port: 5173,
25
25
  proxy: {
26
26
  '/app/api': {
27
- target: 'http://localhost:3004',
27
+ target: 'http://localhost:7404',
28
28
  rewrite: (path) => path.replace(/^\/app/, ''),
29
29
  },
30
- '/api': 'http://localhost:3000',
30
+ '/api': 'http://localhost:7400',
31
31
  },
32
32
  warmup: {
33
33
  clientFiles: ['./src/main.tsx'],
@@ -278,7 +278,7 @@ skills/
278
278
 
279
279
  Only ONE skill can be active for customer-facing mode at a time. The active skill is set in the channel config (`channels.whatsapp.skill`). When your human asks to switch skills, update the config:
280
280
  ```bash
281
- curl -s -X POST http://localhost:3000/api/channels/whatsapp/configure \
281
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
282
282
  -H "Content-Type: application/json" -d '{"skill":"whatsapp-clinic"}'
283
283
  ```
284
284
 
@@ -357,14 +357,14 @@ The format is: `[Channel | phone | role | name (optional)]`
357
357
  ### Setting Up WhatsApp
358
358
 
359
359
  When your human asks to configure WhatsApp:
360
- 1. Start the connection: `curl -s -X POST http://localhost:3000/api/channels/whatsapp/connect`
361
- 2. Tell them to open the QR page: `http://localhost:3000/api/channels/whatsapp/qr-page` (Don't mention the URL until you are actually starting the connection)
360
+ 1. Start the connection: `curl -s -X POST http://localhost:7400/api/channels/whatsapp/connect`
361
+ 2. Tell them to open the QR page: `http://localhost:7400/api/channels/whatsapp/qr-page` (Don't mention the URL until you are actually starting the connection)
362
362
  3. They scan the QR with their WhatsApp app
363
363
  4. The default mode is **channel** (self-chat only)
364
364
 
365
365
  To switch to **business mode** with admin numbers:
366
366
  ```bash
367
- curl -s -X POST http://localhost:3000/api/channels/whatsapp/configure \
367
+ curl -s -X POST http://localhost:7400/api/channels/whatsapp/configure \
368
368
  -H "Content-Type: application/json" \
369
369
  -d '{"mode":"business","admins":["+17865551234","+5511999887766"]}'
370
370
  ```
@@ -373,7 +373,7 @@ curl -s -X POST http://localhost:3000/api/channels/whatsapp/configure \
373
373
 
374
374
  To INITIATE a WhatsApp message (during pulse, cron, or when you want to reach out first):
375
375
  ```bash
376
- curl -s -X POST http://localhost:3000/api/channels/send \
376
+ curl -s -X POST http://localhost:7400/api/channels/send \
377
377
  -H "Content-Type: application/json" \
378
378
  -d '{"channel":"whatsapp","to":"5511999888777","text":"Your appointment is confirmed for tomorrow at 2pm."}'
379
379
  ```
@@ -403,7 +403,7 @@ This is your memory of that customer. Next time they message, read their file fi
403
403
  | `/api/channels/whatsapp/configure` | POST | Set mode + admins array |
404
404
  | `/api/channels/send` | POST | Send proactive message via any channel |
405
405
 
406
- All endpoints are on `http://localhost:3000`.
406
+ All endpoints are on `http://localhost:7400`.
407
407
 
408
408
  ---
409
409
 
@@ -480,7 +480,7 @@ Rules:
480
480
 
481
481
  ## Backend Routing (Critical)
482
482
 
483
- A supervisor process sits in front of everything on port 3000. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
483
+ A supervisor process sits in front of everything on port 7400. It strips the `/app` prefix before forwarding to the backend, preserving the `/api/` path.
484
484
 
485
485
  ```
486
486
  Browser: GET /app/api/tasks → Supervisor strips /app → Backend receives: GET /api/tasks
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import Database from 'better-sqlite3';
5
5
 
6
- const PORT = parseInt(process.env.BACKEND_PORT || '3004', 10);
6
+ const PORT = parseInt(process.env.BACKEND_PORT || '7404', 10);
7
7
  const WORKSPACE = path.resolve(import.meta.dirname, '..');
8
8
 
9
9
  // Load workspace/.env manually (no dotenv dep needed)
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "chrome-extension",
3
+ "version": "1.0.0",
4
+ "description": "Chrome extension that puts Bloby on every webpage. Page context, video transcription, smart browsing assistant.",
5
+ "skills": "./"
6
+ }
@@ -0,0 +1,133 @@
1
+ # Chrome Extension
2
+
3
+ ## What This Is
4
+
5
+ A Chrome extension that puts you (the bloby) on every webpage your human visits. A floating bubble appears in the bottom-right corner of every tab. Your human clicks it, and a chat panel slides out — same chat, same conversation, fully mirrored with the dashboard and WhatsApp.
6
+
7
+ The extension also gives your human the ability to share page context with you — URL, title, selected text, and structured data (product info, prices). This lets you help with what they're actively looking at.
8
+
9
+ ## Dependencies
10
+
11
+ None.
12
+
13
+ ## Setup
14
+
15
+ ### Step 1: Generate a pairing code
16
+
17
+ When your human asks to set up the Chrome extension, generate a pairing code:
18
+
19
+ ```bash
20
+ curl -s -X POST https://api.bloby.bot/api/extension/pair/create \
21
+ -H "Content-Type: application/json" \
22
+ -H "Authorization: Bearer $(cat ~/.bloby/config.json | grep -o '"token":"[^"]*"' | head -1 | cut -d'"' -f4)"
23
+ ```
24
+
25
+ This returns a 6-digit code that expires in 5 minutes.
26
+
27
+ ### Step 2: Walk your human through installation
28
+
29
+ Tell your human:
30
+
31
+ 1. Open `chrome://extensions` in Chrome
32
+ 2. Turn on **Developer Mode** (toggle in the top-right corner)
33
+ 3. Click **Load unpacked**
34
+ 4. Select the folder: `~/.bloby/workspace/skills/chrome-extension/`
35
+ 5. Click the puzzle piece icon in Chrome's toolbar and pin Bloby
36
+ 6. Click the Bloby icon and enter the 6-digit pairing code
37
+
38
+ ### Step 3: Verify it works
39
+
40
+ After your human enters the code, the extension connects to your server. A gradient bubble should appear on every webpage. Ask your human to confirm they see it.
41
+
42
+ If the code expired, generate a new one (repeat Step 1).
43
+
44
+ ### Re-pairing
45
+
46
+ If your human needs to reconnect (new device, token expired):
47
+ 1. Generate a new pairing code (Step 1)
48
+ 2. They click the Bloby icon in Chrome toolbar → enter the new code
49
+
50
+ ## Usage
51
+
52
+ ### How messages from the extension arrive
53
+
54
+ Messages from the extension come through the same WebSocket as dashboard chat messages. There is no difference — the extension is just another connected client. Your responses appear in both the extension and the dashboard simultaneously.
55
+
56
+ ### Page context
57
+
58
+ When your human sends a message from the extension, the message may include page context. This appears as metadata at the start of the message:
59
+
60
+ ```
61
+ [Page: https://amazon.com/dp/B0XXXXX | iPhone 16 Pro - 256GB | $999.00]
62
+ Can you find this cheaper?
63
+ ```
64
+
65
+ The context includes:
66
+ - **URL** — the page they're on
67
+ - **Title** — the page title
68
+ - **Selection** — any text they've highlighted (up to 2000 chars)
69
+ - **Product info** — name, price, currency (extracted from JSON-LD structured data if available)
70
+ - **Meta description** — page summary
71
+
72
+ You can use this context directly, or fetch the URL yourself with curl/fetch tools for more detail.
73
+
74
+ ### Common use cases
75
+
76
+ **Price comparison:**
77
+ Human is on a product page → asks "find this cheaper" → you have the URL, product name, and price → search the web for alternatives.
78
+
79
+ **Page summarization:**
80
+ Human is on a long article → asks "summarize this" → you fetch the URL and produce a summary.
81
+
82
+ **Video transcription:**
83
+ Human is on YouTube → asks "transcribe this" → you extract the video URL, use available tools to get the transcript.
84
+
85
+ **Save to workspace:**
86
+ Human finds something interesting → asks "save this to my research notes" → you write to a file in the workspace.
87
+
88
+ **Shopping assistant:**
89
+ Human is browsing → asks "add this to my wishlist" → you maintain a wishlist file in the workspace.
90
+
91
+ **Research assistant:**
92
+ Human is reading documentation → highlights text → asks "explain this" → you get the selected text and explain it in context.
93
+
94
+ ### Working with the page URL
95
+
96
+ You have full tool access. If the context isn't enough, fetch the page yourself:
97
+
98
+ ```bash
99
+ curl -s "https://the-url-from-context" | head -200
100
+ ```
101
+
102
+ Or use an MCP fetch tool if configured.
103
+
104
+ ## Human Interaction
105
+
106
+ ### What to tell your human
107
+
108
+ When they ask about the extension:
109
+ - "I can see whatever page you're on and help you with it"
110
+ - "Just click my bubble on any page and ask me anything"
111
+ - "If you highlight text on a page, I can see what you selected"
112
+ - "I can find better prices, summarize articles, save things to your workspace — whatever you need"
113
+
114
+ ### What NOT to say
115
+
116
+ Never mention "extension", "content script", "background worker", or any technical implementation details. From your human's perspective, you're just... everywhere. Available on every page. That's it.
117
+
118
+ ### Permissions
119
+
120
+ The extension only reads page content when your human actively opens the chat panel. It does NOT:
121
+ - Track browsing history
122
+ - Read pages in the background
123
+ - Send data anywhere except to your server
124
+ - Access passwords, form data, or cookies
125
+
126
+ If your human asks about privacy, be transparent about this.
127
+
128
+ ## Notes
129
+
130
+ - The extension connects via the relay URL (e.g., `bloby.bloby.bot`). If the relay is down or the tunnel is offline, the extension chat won't connect. The bubble still appears but messages won't send.
131
+ - The pairing code is single-use and expires in 5 minutes. Generate a new one if it expires.
132
+ - The extension works on all pages except Chrome internal pages (`chrome://`, `chrome-extension://`) and your own Bloby dashboard domain (to avoid double-bubble).
133
+ - Multiple browsers/devices can be paired — each gets its own pairing code. They all share the same conversation.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Bloby Chrome Extension — Background Service Worker
3
+ *
4
+ * Responsibilities:
5
+ * - Manages the WebSocket connection to the user's Bloby server
6
+ * - Bridges messages between content scripts and the server
7
+ * - Handles pairing state (serverUrl stored in chrome.storage.local)
8
+ * - Keeps WS alive with heartbeat pings
9
+ */
10
+
11
+ let ws = null;
12
+ let config = null; // { serverUrl, username, tier }
13
+ let reconnectTimer = null;
14
+ let reconnectDelay = 1000;
15
+ const MAX_RECONNECT_DELAY = 8000;
16
+ const HEARTBEAT_MS = 25000;
17
+ let heartbeatTimer = null;
18
+
19
+ // ── Config Management ──────────────────────────────────────────────────────
20
+
21
+ async function loadConfig() {
22
+ const data = await chrome.storage.local.get(['serverUrl', 'username', 'tier', 'authToken']);
23
+ if (data.serverUrl) {
24
+ config = data;
25
+ console.log(`[bloby-bg] Config loaded: ${config.serverUrl} (${config.username})`);
26
+ return true;
27
+ }
28
+ console.log('[bloby-bg] No config found — not paired');
29
+ return false;
30
+ }
31
+
32
+ async function saveConfig(newConfig) {
33
+ config = newConfig;
34
+ await chrome.storage.local.set(newConfig);
35
+ console.log(`[bloby-bg] Config saved: ${config.serverUrl}`);
36
+ }
37
+
38
+ async function clearConfig() {
39
+ config = null;
40
+ await chrome.storage.local.clear();
41
+ disconnectWs();
42
+ console.log('[bloby-bg] Config cleared — unpaired');
43
+ }
44
+
45
+ // ── WebSocket Connection ───────────────────────────────────────────────────
46
+
47
+ function connectWs() {
48
+ if (!config?.serverUrl) return;
49
+ if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
50
+
51
+ const proto = config.serverUrl.startsWith('https') ? 'wss' : 'ws';
52
+ const host = config.serverUrl.replace(/^https?:\/\//, '');
53
+ let url = `${proto}://${host}/bloby/ws`;
54
+ if (config.authToken) {
55
+ url += `?token=${config.authToken}`;
56
+ }
57
+
58
+ console.log(`[bloby-bg] Connecting WS: ${url.replace(/token=.*/, 'token=***')}`);
59
+
60
+ try {
61
+ ws = new WebSocket(url);
62
+ } catch (err) {
63
+ console.error('[bloby-bg] WS constructor error:', err);
64
+ scheduleReconnect();
65
+ return;
66
+ }
67
+
68
+ ws.onopen = () => {
69
+ console.log('[bloby-bg] WS connected');
70
+ reconnectDelay = 1000;
71
+ startHeartbeat();
72
+ // Notify all tabs
73
+ broadcastToTabs({ type: 'bloby:ws-connected' });
74
+ };
75
+
76
+ ws.onmessage = (event) => {
77
+ try {
78
+ const msg = JSON.parse(event.data);
79
+ // Forward all server messages to content scripts
80
+ broadcastToTabs(msg);
81
+ } catch { /* ignore non-JSON */ }
82
+ };
83
+
84
+ ws.onclose = (event) => {
85
+ console.log(`[bloby-bg] WS closed: code=${event.code} reason=${event.reason}`);
86
+ stopHeartbeat();
87
+ ws = null;
88
+ broadcastToTabs({ type: 'bloby:ws-disconnected' });
89
+ scheduleReconnect();
90
+ };
91
+
92
+ ws.onerror = (err) => {
93
+ console.error('[bloby-bg] WS error');
94
+ // onclose will fire after this
95
+ };
96
+ }
97
+
98
+ function disconnectWs() {
99
+ stopHeartbeat();
100
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
101
+ if (ws) {
102
+ ws.onclose = null; // prevent reconnect
103
+ ws.close();
104
+ ws = null;
105
+ }
106
+ }
107
+
108
+ function scheduleReconnect() {
109
+ if (reconnectTimer) return;
110
+ console.log(`[bloby-bg] Reconnecting in ${reconnectDelay}ms...`);
111
+ reconnectTimer = setTimeout(() => {
112
+ reconnectTimer = null;
113
+ reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
114
+ connectWs();
115
+ }, reconnectDelay);
116
+ }
117
+
118
+ function startHeartbeat() {
119
+ stopHeartbeat();
120
+ heartbeatTimer = setInterval(() => {
121
+ if (ws?.readyState === WebSocket.OPEN) {
122
+ ws.send(JSON.stringify({ type: 'ping' }));
123
+ }
124
+ }, HEARTBEAT_MS);
125
+ }
126
+
127
+ function stopHeartbeat() {
128
+ if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
129
+ }
130
+
131
+ // ── Send to Server ─────────────────────────────────────────────────────────
132
+
133
+ function sendToServer(msg) {
134
+ if (ws?.readyState === WebSocket.OPEN) {
135
+ ws.send(JSON.stringify(msg));
136
+ return true;
137
+ }
138
+ console.warn('[bloby-bg] WS not connected — message dropped');
139
+ return false;
140
+ }
141
+
142
+ // ── Broadcast to All Tabs ──────────────────────────────────────────────────
143
+
144
+ async function broadcastToTabs(msg) {
145
+ try {
146
+ const tabs = await chrome.tabs.query({});
147
+ for (const tab of tabs) {
148
+ if (tab.id) {
149
+ chrome.tabs.sendMessage(tab.id, msg).catch(() => {
150
+ // Tab doesn't have content script — ignore
151
+ });
152
+ }
153
+ }
154
+ } catch { /* ignore */ }
155
+ }
156
+
157
+ // ── Message Handler (from content scripts + popup) ─────────────────────────
158
+
159
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
160
+ // Popup: get current state
161
+ if (msg.type === 'bloby:get-state') {
162
+ sendResponse({
163
+ paired: !!config?.serverUrl,
164
+ connected: ws?.readyState === WebSocket.OPEN,
165
+ config: config ? { serverUrl: config.serverUrl, username: config.username } : null,
166
+ });
167
+ return true;
168
+ }
169
+
170
+ // Popup: pair with code
171
+ if (msg.type === 'bloby:pair') {
172
+ handlePair(msg.code).then(sendResponse);
173
+ return true; // async response
174
+ }
175
+
176
+ // Popup: unpair
177
+ if (msg.type === 'bloby:unpair') {
178
+ clearConfig().then(() => sendResponse({ success: true }));
179
+ return true;
180
+ }
181
+
182
+ // Content script: send message to server
183
+ if (msg.type === 'bloby:send') {
184
+ const ok = sendToServer(msg.payload);
185
+ sendResponse({ sent: ok });
186
+ return false;
187
+ }
188
+
189
+ // Content script: get page context
190
+ if (msg.type === 'bloby:page-context') {
191
+ // Content script provides context, we just forward
192
+ sendResponse({ received: true });
193
+ return false;
194
+ }
195
+ });
196
+
197
+ // ── Pairing Flow ───────────────────────────────────────────────────────────
198
+
199
+ async function handlePair(code) {
200
+ try {
201
+ console.log(`[bloby-bg] Verifying pairing code: ${code}`);
202
+
203
+ const res = await fetch('https://api.bloby.bot/api/extension/pair/verify', {
204
+ method: 'POST',
205
+ headers: { 'Content-Type': 'application/json' },
206
+ body: JSON.stringify({ code }),
207
+ });
208
+
209
+ if (!res.ok) {
210
+ const data = await res.json().catch(() => ({}));
211
+ console.log(`[bloby-bg] Pairing failed: ${data.error || res.status}`);
212
+ return { success: false, error: data.error || 'Invalid code' };
213
+ }
214
+
215
+ const data = await res.json();
216
+ console.log(`[bloby-bg] Pairing verified: ${data.serverUrl}`);
217
+
218
+ await saveConfig({
219
+ serverUrl: data.serverUrl,
220
+ username: data.username,
221
+ tier: data.tier,
222
+ });
223
+
224
+ // Connect immediately
225
+ connectWs();
226
+
227
+ return { success: true, username: data.username, serverUrl: data.serverUrl };
228
+ } catch (err) {
229
+ console.error('[bloby-bg] Pairing error:', err);
230
+ return { success: false, error: 'Network error — check your connection' };
231
+ }
232
+ }
233
+
234
+ // ── Startup ────────────────────────────────────────────────────────────────
235
+
236
+ (async () => {
237
+ const hasCfg = await loadConfig();
238
+ if (hasCfg) connectWs();
239
+ })();