bloby-bot 0.22.0 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bloby-bot",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "releaseNotes": [
5
5
  "1. react router implemented",
6
6
  "2. new workspace design",
@@ -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
+ })();
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Bloby Chrome Extension — Content Script
3
+ *
4
+ * Injected into every page. Responsibilities:
5
+ * - Renders the Bloby bubble (bottom-right corner)
6
+ * - Opens/closes the chat panel (iframe to extension's panel.html)
7
+ * - Extracts page context (URL, title, selection) for the agent
8
+ * - Bridges messages between the panel iframe and the background worker
9
+ */
10
+
11
+ (function () {
12
+ // Guard: don't inject on extension pages or Bloby's own domain
13
+ if (document.getElementById('bloby-ext-bubble')) return;
14
+ if (location.hostname.endsWith('bloby.bot')) return;
15
+
16
+ const BUBBLE_SIZE = 60;
17
+ const BUBBLE_MARGIN = 24;
18
+ const PANEL_WIDTH = 420;
19
+
20
+ let panelOpen = false;
21
+ let paired = false;
22
+
23
+ // ── Check if paired ────────────────────────────────────────────────────────
24
+
25
+ chrome.runtime.sendMessage({ type: 'bloby:get-state' }, (state) => {
26
+ if (chrome.runtime.lastError) return;
27
+ if (state?.paired) {
28
+ paired = true;
29
+ createBubble();
30
+ }
31
+ });
32
+
33
+ // Listen for pairing changes
34
+ chrome.storage.onChanged.addListener((changes) => {
35
+ if (changes.serverUrl) {
36
+ if (changes.serverUrl.newValue) {
37
+ paired = true;
38
+ if (!document.getElementById('bloby-ext-bubble')) createBubble();
39
+ } else {
40
+ paired = false;
41
+ removeBubble();
42
+ }
43
+ }
44
+ });
45
+
46
+ // ── Bubble ─────────────────────────────────────────────────────────────────
47
+
48
+ function createBubble() {
49
+ const bubble = document.createElement('div');
50
+ bubble.id = 'bloby-ext-bubble';
51
+ bubble.title = 'Chat with Bloby';
52
+ bubble.addEventListener('click', togglePanel);
53
+ document.body.appendChild(bubble);
54
+
55
+ // Inner dot (gradient)
56
+ const dot = document.createElement('div');
57
+ dot.className = 'bloby-ext-bubble-dot';
58
+ bubble.appendChild(dot);
59
+ }
60
+
61
+ function removeBubble() {
62
+ const bubble = document.getElementById('bloby-ext-bubble');
63
+ if (bubble) bubble.remove();
64
+ const panel = document.getElementById('bloby-ext-panel');
65
+ if (panel) panel.remove();
66
+ const backdrop = document.getElementById('bloby-ext-backdrop');
67
+ if (backdrop) backdrop.remove();
68
+ }
69
+
70
+ // ── Panel ──────────────────────────────────────────────────────────────────
71
+
72
+ function togglePanel() {
73
+ if (panelOpen) {
74
+ closePanel();
75
+ } else {
76
+ openPanel();
77
+ }
78
+ }
79
+
80
+ function openPanel() {
81
+ if (document.getElementById('bloby-ext-panel')) {
82
+ const panel = document.getElementById('bloby-ext-panel');
83
+ panel.classList.add('bloby-ext-panel-open');
84
+ panelOpen = true;
85
+ showBackdrop();
86
+ return;
87
+ }
88
+
89
+ // Create backdrop
90
+ showBackdrop();
91
+
92
+ // Create panel
93
+ const panel = document.createElement('div');
94
+ panel.id = 'bloby-ext-panel';
95
+
96
+ // Create iframe — loads the extension's own panel page
97
+ const iframe = document.createElement('iframe');
98
+ iframe.id = 'bloby-ext-iframe';
99
+ iframe.src = chrome.runtime.getURL('panel/panel.html');
100
+ iframe.allow = 'microphone';
101
+ panel.appendChild(iframe);
102
+
103
+ document.body.appendChild(panel);
104
+
105
+ // Trigger slide-in animation on next frame
106
+ requestAnimationFrame(() => {
107
+ panel.classList.add('bloby-ext-panel-open');
108
+ panelOpen = true;
109
+ });
110
+
111
+ // Listen for messages from the panel iframe
112
+ window.addEventListener('message', handlePanelMessage);
113
+ }
114
+
115
+ function closePanel() {
116
+ const panel = document.getElementById('bloby-ext-panel');
117
+ if (panel) {
118
+ panel.classList.remove('bloby-ext-panel-open');
119
+ panelOpen = false;
120
+ hideBackdrop();
121
+ }
122
+ }
123
+
124
+ function showBackdrop() {
125
+ let backdrop = document.getElementById('bloby-ext-backdrop');
126
+ if (!backdrop) {
127
+ backdrop = document.createElement('div');
128
+ backdrop.id = 'bloby-ext-backdrop';
129
+ backdrop.addEventListener('click', closePanel);
130
+ document.body.appendChild(backdrop);
131
+ }
132
+ requestAnimationFrame(() => backdrop.classList.add('bloby-ext-backdrop-visible'));
133
+ }
134
+
135
+ function hideBackdrop() {
136
+ const backdrop = document.getElementById('bloby-ext-backdrop');
137
+ if (backdrop) {
138
+ backdrop.classList.remove('bloby-ext-backdrop-visible');
139
+ }
140
+ }
141
+
142
+ // ── Panel ↔ Background Message Bridge ──────────────────────────────────────
143
+
144
+ function handlePanelMessage(event) {
145
+ if (event.source !== document.getElementById('bloby-ext-iframe')?.contentWindow) return;
146
+
147
+ const msg = event.data;
148
+ if (!msg?.type) return;
149
+
150
+ // Close panel request
151
+ if (msg.type === 'bloby:close') {
152
+ closePanel();
153
+ return;
154
+ }
155
+
156
+ // Forward chat messages to background worker → server
157
+ if (msg.type === 'bloby:send') {
158
+ chrome.runtime.sendMessage({ type: 'bloby:send', payload: msg.payload });
159
+ return;
160
+ }
161
+
162
+ // Panel requests page context
163
+ if (msg.type === 'bloby:get-page-context') {
164
+ const context = getPageContext();
165
+ const iframe = document.getElementById('bloby-ext-iframe');
166
+ if (iframe?.contentWindow) {
167
+ iframe.contentWindow.postMessage({ type: 'bloby:page-context', context }, '*');
168
+ }
169
+ return;
170
+ }
171
+ }
172
+
173
+ // Forward server messages from background to panel iframe
174
+ chrome.runtime.onMessage.addListener((msg) => {
175
+ const iframe = document.getElementById('bloby-ext-iframe');
176
+ if (iframe?.contentWindow && panelOpen) {
177
+ iframe.contentWindow.postMessage(msg, '*');
178
+ }
179
+ });
180
+
181
+ // ── Page Context Extraction ────────────────────────────────────────────────
182
+
183
+ function getPageContext() {
184
+ const context = {
185
+ url: location.href,
186
+ title: document.title,
187
+ selection: window.getSelection()?.toString()?.slice(0, 2000) || '',
188
+ };
189
+
190
+ // Try to extract structured data (JSON-LD)
191
+ const jsonLd = document.querySelector('script[type="application/ld+json"]');
192
+ if (jsonLd) {
193
+ try {
194
+ const data = JSON.parse(jsonLd.textContent);
195
+ if (data.name) context.productName = data.name;
196
+ if (data.offers?.price) context.price = data.offers.price;
197
+ if (data.offers?.priceCurrency) context.currency = data.offers.priceCurrency;
198
+ if (data.description) context.description = data.description?.slice(0, 500);
199
+ } catch { /* ignore malformed JSON-LD */ }
200
+ }
201
+
202
+ // Meta description fallback
203
+ const metaDesc = document.querySelector('meta[name="description"]');
204
+ if (metaDesc && !context.description) {
205
+ context.description = metaDesc.content?.slice(0, 500);
206
+ }
207
+
208
+ return context;
209
+ }
210
+
211
+ // ── Keyboard shortcut ──────────────────────────────────────────────────────
212
+
213
+ document.addEventListener('keydown', (e) => {
214
+ if (e.key === 'Escape' && panelOpen) {
215
+ closePanel();
216
+ }
217
+ });
218
+ })();
@@ -0,0 +1,86 @@
1
+ /* Bloby Chrome Extension — Bubble & Panel Styles */
2
+
3
+ #bloby-ext-bubble {
4
+ position: fixed;
5
+ bottom: 24px;
6
+ right: 24px;
7
+ width: 60px;
8
+ height: 60px;
9
+ border-radius: 50%;
10
+ background: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
11
+ cursor: pointer;
12
+ z-index: 2147483646;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
17
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
18
+ }
19
+
20
+ #bloby-ext-bubble:hover {
21
+ transform: scale(1.08);
22
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4);
23
+ }
24
+
25
+ #bloby-ext-bubble:active {
26
+ transform: scale(0.95);
27
+ }
28
+
29
+ .bloby-ext-bubble-dot {
30
+ width: 24px;
31
+ height: 24px;
32
+ border-radius: 50%;
33
+ background: rgba(255, 255, 255, 0.9);
34
+ }
35
+
36
+ /* Panel */
37
+ #bloby-ext-panel {
38
+ position: fixed;
39
+ top: 0;
40
+ right: 0;
41
+ bottom: 0;
42
+ width: 420px;
43
+ z-index: 2147483647;
44
+ transform: translateX(100%);
45
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
46
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.3);
47
+ border-left: 1px solid #3a3a3a;
48
+ background: #212121;
49
+ }
50
+
51
+ #bloby-ext-panel.bloby-ext-panel-open {
52
+ transform: translateX(0);
53
+ }
54
+
55
+ /* Mobile: full width */
56
+ @media (max-width: 480px) {
57
+ #bloby-ext-panel {
58
+ width: 100vw;
59
+ }
60
+ }
61
+
62
+ #bloby-ext-iframe {
63
+ width: 100%;
64
+ height: 100%;
65
+ border: none;
66
+ background: #212121;
67
+ }
68
+
69
+ /* Backdrop */
70
+ #bloby-ext-backdrop {
71
+ position: fixed;
72
+ top: 0;
73
+ left: 0;
74
+ right: 0;
75
+ bottom: 0;
76
+ background: rgba(0, 0, 0, 0.4);
77
+ z-index: 2147483645;
78
+ opacity: 0;
79
+ pointer-events: none;
80
+ transition: opacity 0.25s ease;
81
+ }
82
+
83
+ #bloby-ext-backdrop.bloby-ext-backdrop-visible {
84
+ opacity: 1;
85
+ pointer-events: auto;
86
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Bloby",
4
+ "version": "0.1.0",
5
+ "description": "Your AI agent, on every page. Chat with Bloby from anywhere.",
6
+ "permissions": [
7
+ "storage",
8
+ "activeTab"
9
+ ],
10
+ "host_permissions": [
11
+ "https://*.bloby.bot/*",
12
+ "https://*.my.bloby.bot/*",
13
+ "https://*.trycloudflare.com/*"
14
+ ],
15
+ "action": {
16
+ "default_popup": "popup/popup.html",
17
+ "default_icon": {
18
+ "16": "icons/icon-16.png",
19
+ "48": "icons/icon-48.png",
20
+ "128": "icons/icon-128.png"
21
+ }
22
+ },
23
+ "background": {
24
+ "service_worker": "background.js",
25
+ "type": "module"
26
+ },
27
+ "content_scripts": [
28
+ {
29
+ "matches": ["<all_urls>"],
30
+ "js": ["content-script.js"],
31
+ "css": ["content-style.css"],
32
+ "run_at": "document_idle"
33
+ }
34
+ ],
35
+ "web_accessible_resources": [
36
+ {
37
+ "resources": ["panel/panel.html", "assets/*", "icons/*"],
38
+ "matches": ["<all_urls>"]
39
+ }
40
+ ],
41
+ "icons": {
42
+ "16": "icons/icon-16.png",
43
+ "48": "icons/icon-48.png",
44
+ "128": "icons/icon-128.png"
45
+ }
46
+ }
@@ -0,0 +1,74 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Bloby</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { background: #212121; width: 100%; height: 100vh; overflow: hidden; }
10
+ iframe { width: 100%; height: 100%; border: none; }
11
+ .loading {
12
+ display: flex; align-items: center; justify-content: center;
13
+ height: 100vh; color: #a1a1aa; font-family: system-ui, sans-serif;
14
+ font-size: 14px;
15
+ }
16
+ .loading .dots span {
17
+ display: inline-block; width: 6px; height: 6px; border-radius: 50%;
18
+ background: #a1a1aa; margin: 0 3px; animation: bounce 1s infinite;
19
+ }
20
+ .loading .dots span:nth-child(2) { animation-delay: 0.15s; }
21
+ .loading .dots span:nth-child(3) { animation-delay: 0.3s; }
22
+ @keyframes bounce {
23
+ 0%, 80%, 100% { transform: scale(0); }
24
+ 40% { transform: scale(1); }
25
+ }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div class="loading" id="loading">
30
+ <div class="dots"><span></span><span></span><span></span></div>
31
+ </div>
32
+
33
+ <script>
34
+ /**
35
+ * Panel page — loads the user's Bloby chat in an iframe.
36
+ * Reads serverUrl from chrome.storage.local.
37
+ */
38
+ (async () => {
39
+ const data = await chrome.storage.local.get(['serverUrl', 'authToken']);
40
+
41
+ if (!data.serverUrl) {
42
+ document.getElementById('loading').innerHTML =
43
+ '<span style="color:#ef4444">Not paired. Click the Bloby icon in the toolbar to set up.</span>';
44
+ return;
45
+ }
46
+
47
+ // Build the chat URL — same as the dashboard chat iframe
48
+ let chatUrl = `${data.serverUrl}/bloby/`;
49
+ if (data.authToken) {
50
+ chatUrl += `?token=${data.authToken}`;
51
+ }
52
+
53
+ // Create iframe to the user's Bloby server
54
+ const iframe = document.createElement('iframe');
55
+ iframe.src = chatUrl;
56
+ iframe.allow = 'microphone';
57
+ iframe.style.cssText = 'width:100%;height:100%;border:none;';
58
+
59
+ iframe.onload = () => {
60
+ document.getElementById('loading').style.display = 'none';
61
+ };
62
+
63
+ document.body.appendChild(iframe);
64
+
65
+ // Forward close events from chat iframe to content script (parent of this panel)
66
+ window.addEventListener('message', (e) => {
67
+ if (e.data?.type === 'bloby:close') {
68
+ window.parent.postMessage({ type: 'bloby:close' }, '*');
69
+ }
70
+ });
71
+ })();
72
+ </script>
73
+ </body>
74
+ </html>
@@ -0,0 +1,124 @@
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ width: 320px;
9
+ min-height: 280px;
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: #0a0a0b;
12
+ color: #e4e4e7;
13
+ padding: 32px 24px;
14
+ text-align: center;
15
+ }
16
+
17
+ .logo {
18
+ display: flex;
19
+ justify-content: center;
20
+ margin-bottom: 20px;
21
+ }
22
+
23
+ .logo-dot {
24
+ width: 48px;
25
+ height: 48px;
26
+ border-radius: 50%;
27
+ background: linear-gradient(135deg, #04D1FE, #AF27E3, #FB4072);
28
+ }
29
+
30
+ .logo-dot.connected {
31
+ box-shadow: 0 0 20px rgba(4, 209, 254, 0.4);
32
+ }
33
+
34
+ h1 {
35
+ font-size: 18px;
36
+ font-weight: 600;
37
+ margin-bottom: 6px;
38
+ }
39
+
40
+ .subtitle {
41
+ font-size: 13px;
42
+ color: #71717a;
43
+ margin-bottom: 24px;
44
+ }
45
+
46
+ /* Code Input */
47
+ .code-input {
48
+ display: flex;
49
+ gap: 8px;
50
+ justify-content: center;
51
+ margin-bottom: 16px;
52
+ }
53
+
54
+ .code-input input {
55
+ width: 40px;
56
+ height: 48px;
57
+ border: 2px solid #27272a;
58
+ border-radius: 10px;
59
+ background: #18181b;
60
+ color: #e4e4e7;
61
+ font-size: 22px;
62
+ font-weight: 600;
63
+ text-align: center;
64
+ outline: none;
65
+ transition: border-color 0.15s;
66
+ caret-color: transparent;
67
+ }
68
+
69
+ .code-input input:focus {
70
+ border-color: #AF27E3;
71
+ }
72
+
73
+ .code-input input.filled {
74
+ border-color: #04D1FE;
75
+ }
76
+
77
+ .error {
78
+ color: #ef4444;
79
+ font-size: 13px;
80
+ min-height: 18px;
81
+ margin-bottom: 8px;
82
+ }
83
+
84
+ .pairing {
85
+ color: #a1a1aa;
86
+ font-size: 13px;
87
+ }
88
+
89
+ /* Connected Screen */
90
+ .status-badge {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 6px;
94
+ background: #18181b;
95
+ border: 1px solid #27272a;
96
+ border-radius: 999px;
97
+ padding: 6px 14px;
98
+ font-size: 13px;
99
+ color: #a1a1aa;
100
+ margin-bottom: 20px;
101
+ }
102
+
103
+ .status-dot {
104
+ width: 8px;
105
+ height: 8px;
106
+ border-radius: 50%;
107
+ background: #22c55e;
108
+ }
109
+
110
+ .btn-disconnect {
111
+ background: none;
112
+ border: 1px solid #27272a;
113
+ border-radius: 8px;
114
+ color: #71717a;
115
+ padding: 8px 16px;
116
+ font-size: 13px;
117
+ cursor: pointer;
118
+ transition: all 0.15s;
119
+ }
120
+
121
+ .btn-disconnect:hover {
122
+ border-color: #ef4444;
123
+ color: #ef4444;
124
+ }
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Bloby</title>
7
+ <link rel="stylesheet" href="popup.css">
8
+ </head>
9
+ <body>
10
+ <!-- Pairing Screen -->
11
+ <div id="pair-screen">
12
+ <div class="logo">
13
+ <div class="logo-dot"></div>
14
+ </div>
15
+ <h1>Connect to Bloby</h1>
16
+ <p class="subtitle">Ask your Bloby for a pairing code</p>
17
+
18
+ <div class="code-input" id="code-input">
19
+ <input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric" autofocus>
20
+ <input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
21
+ <input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
22
+ <input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
23
+ <input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
24
+ <input type="text" maxlength="1" pattern="[0-9]" inputmode="numeric">
25
+ </div>
26
+
27
+ <div id="error" class="error"></div>
28
+ <div id="pairing" class="pairing" style="display:none">Connecting...</div>
29
+ </div>
30
+
31
+ <!-- Connected Screen -->
32
+ <div id="connected-screen" style="display:none">
33
+ <div class="logo">
34
+ <div class="logo-dot connected"></div>
35
+ </div>
36
+ <h1 id="connected-name">Connected</h1>
37
+ <p class="subtitle" id="connected-url"></p>
38
+ <div class="status-badge">
39
+ <span class="status-dot"></span>
40
+ <span id="status-text">Connected</span>
41
+ </div>
42
+ <button id="disconnect-btn" class="btn-disconnect">Disconnect</button>
43
+ </div>
44
+
45
+ <script src="popup.js"></script>
46
+ </body>
47
+ </html>
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Bloby Chrome Extension — Popup
3
+ *
4
+ * Shows either:
5
+ * - Pairing screen: 6-digit code input to connect to a Bloby instance
6
+ * - Connected screen: status + disconnect button
7
+ */
8
+
9
+ const pairScreen = document.getElementById('pair-screen');
10
+ const connectedScreen = document.getElementById('connected-screen');
11
+ const errorEl = document.getElementById('error');
12
+ const pairingEl = document.getElementById('pairing');
13
+ const inputs = document.querySelectorAll('.code-input input');
14
+
15
+ // ── Init: check current state ──────────────────────────────────────────────
16
+
17
+ chrome.runtime.sendMessage({ type: 'bloby:get-state' }, (state) => {
18
+ if (state?.paired) {
19
+ showConnected(state);
20
+ } else {
21
+ showPairing();
22
+ }
23
+ });
24
+
25
+ // ── Pairing Screen ─────────────────────────────────────────────────────────
26
+
27
+ function showPairing() {
28
+ pairScreen.style.display = 'block';
29
+ connectedScreen.style.display = 'none';
30
+ inputs[0].focus();
31
+ }
32
+
33
+ // Auto-advance between code inputs
34
+ inputs.forEach((input, i) => {
35
+ input.addEventListener('input', (e) => {
36
+ const val = e.target.value.replace(/\D/g, '');
37
+ e.target.value = val;
38
+
39
+ if (val) {
40
+ e.target.classList.add('filled');
41
+ if (i < inputs.length - 1) {
42
+ inputs[i + 1].focus();
43
+ }
44
+ }
45
+
46
+ // Check if all filled
47
+ const code = Array.from(inputs).map((inp) => inp.value).join('');
48
+ if (code.length === 6) {
49
+ submitCode(code);
50
+ }
51
+ });
52
+
53
+ input.addEventListener('keydown', (e) => {
54
+ if (e.key === 'Backspace' && !e.target.value && i > 0) {
55
+ inputs[i - 1].focus();
56
+ inputs[i - 1].value = '';
57
+ inputs[i - 1].classList.remove('filled');
58
+ }
59
+ });
60
+
61
+ // Handle paste
62
+ input.addEventListener('paste', (e) => {
63
+ e.preventDefault();
64
+ const pasted = (e.clipboardData.getData('text') || '').replace(/\D/g, '').slice(0, 6);
65
+ pasted.split('').forEach((char, j) => {
66
+ if (inputs[j]) {
67
+ inputs[j].value = char;
68
+ inputs[j].classList.add('filled');
69
+ }
70
+ });
71
+ if (pasted.length === 6) {
72
+ submitCode(pasted);
73
+ } else if (pasted.length > 0) {
74
+ inputs[Math.min(pasted.length, 5)].focus();
75
+ }
76
+ });
77
+ });
78
+
79
+ async function submitCode(code) {
80
+ errorEl.textContent = '';
81
+ pairingEl.style.display = 'block';
82
+ inputs.forEach((inp) => { inp.disabled = true; });
83
+
84
+ chrome.runtime.sendMessage({ type: 'bloby:pair', code }, (result) => {
85
+ pairingEl.style.display = 'none';
86
+
87
+ if (result?.success) {
88
+ showConnected({
89
+ config: { serverUrl: result.serverUrl, username: result.username },
90
+ connected: true,
91
+ });
92
+ } else {
93
+ errorEl.textContent = result?.error || 'Pairing failed';
94
+ inputs.forEach((inp) => { inp.disabled = false; inp.value = ''; inp.classList.remove('filled'); });
95
+ inputs[0].focus();
96
+ }
97
+ });
98
+ }
99
+
100
+ // ── Connected Screen ───────────────────────────────────────────────────────
101
+
102
+ function showConnected(state) {
103
+ pairScreen.style.display = 'none';
104
+ connectedScreen.style.display = 'block';
105
+
106
+ document.getElementById('connected-name').textContent = state.config?.username || 'Connected';
107
+ document.getElementById('connected-url').textContent = state.config?.serverUrl || '';
108
+ document.getElementById('status-text').textContent = state.connected ? 'Connected' : 'Offline';
109
+ }
110
+
111
+ document.getElementById('disconnect-btn').addEventListener('click', () => {
112
+ chrome.runtime.sendMessage({ type: 'bloby:unpair' }, () => {
113
+ showPairing();
114
+ });
115
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "chrome-extension",
3
+ "version": "1.0.0",
4
+ "bloby_human": "Bruno Bertapeli",
5
+ "bloby": "bloby-bruno",
6
+ "author": "newbot-official",
7
+ "description": "Bloby on every webpage. Browse with your AI agent — transcribe videos, compare prices, summarize articles, and more.",
8
+ "type": "skill",
9
+ "depends": [],
10
+ "env_keys": [],
11
+ "has_telemetry": false,
12
+ "size": "32KB",
13
+ "contains_binaries": false,
14
+ "tags": ["chrome", "extension", "browser", "productivity", "assistant"]
15
+ }