bloby-bot 0.22.0 → 0.22.2
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/dist-bloby/assets/{bloby-8izT4vax.js → bloby-DvSkie1b.js} +7 -5
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-C3HbR2K9.js → highlighted-body-OFNGDK62-BbEkUgTM.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-C0VBooMz.js +1 -0
- package/dist-bloby/bloby.html +1 -1
- package/package.json +5 -5
- package/supervisor/chat/src/hooks/useBlobyChat.ts +40 -1
- package/supervisor/index.ts +1 -1
- package/workspace/skills/chrome-extension/.claude-plugin/plugin.json +6 -0
- package/workspace/skills/chrome-extension/SKILL.md +181 -0
- package/workspace/skills/chrome-extension/background.js +182 -0
- package/workspace/skills/chrome-extension/content-script.js +309 -0
- package/workspace/skills/chrome-extension/content-style.css +86 -0
- package/workspace/skills/chrome-extension/icons/icon-128.png +0 -0
- package/workspace/skills/chrome-extension/icons/icon-16.png +0 -0
- package/workspace/skills/chrome-extension/icons/icon-48.png +0 -0
- package/workspace/skills/chrome-extension/manifest.json +47 -0
- package/workspace/skills/chrome-extension/panel/panel.html +36 -0
- package/workspace/skills/chrome-extension/panel/panel.js +123 -0
- package/workspace/skills/chrome-extension/popup/popup.css +124 -0
- package/workspace/skills/chrome-extension/popup/popup.html +47 -0
- package/workspace/skills/chrome-extension/popup/popup.js +115 -0
- package/workspace/skills/chrome-extension/skill.json +15 -0
- package/dist-bloby/assets/mermaid-GHXKKRXX-BX4zpcbf.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-
|
|
1
|
+
import{c as e,r as t,t as n}from"./jsx-runtime-C0W9Wf2W.js";import{n as r,r as i,t as a}from"./bloby-DvSkie1b.js";var o=e(t(),1),s=n(),c=({code:e,language:t,raw:n,className:c,startLine:l,lineNumbers:u,...d})=>{let{shikiTheme:f}=(0,o.useContext)(i),p=r(),[m,h]=(0,o.useState)(n);return(0,o.useEffect)(()=>{if(!p){h(n);return}let r=p.highlight({code:e,language:t,themes:f},e=>{h(e)});r&&h(r)},[e,t,f,p,n]),(0,s.jsx)(a,{className:c,language:t,lineNumbers:u,result:m,startLine:l,...d})};export{c as HighlightedCodeBlockBody};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e}from"./bloby-DvSkie1b.js";export{e as Mermaid};
|
package/dist-bloby/bloby.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Bloby Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/bloby/assets/bloby-
|
|
7
|
+
<script type="module" crossorigin src="/bloby/assets/bloby-DvSkie1b.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/bloby/assets/jsx-runtime-C0W9Wf2W.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/bloby/assets/globals-VdwDxdso.js">
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/bloby/assets/globals-b7xkhPEo.css">
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bloby-bot",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.2",
|
|
4
4
|
"releaseNotes": [
|
|
5
|
-
"1.
|
|
6
|
-
"2.
|
|
7
|
-
"3.
|
|
8
|
-
"4.
|
|
5
|
+
"1. testing chrome extension",
|
|
6
|
+
"2. ",
|
|
7
|
+
"3. ",
|
|
8
|
+
"4. "
|
|
9
9
|
],
|
|
10
10
|
"description": "Self-hosted, self-evolving AI agent with its own dashboard.",
|
|
11
11
|
"type": "module",
|
|
@@ -23,11 +23,33 @@ export function useBlobyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
23
23
|
const streamBufferRef = useRef('');
|
|
24
24
|
/** Length of text already committed as a partial message — strip from next bot:response */
|
|
25
25
|
const committedTextLength = useRef(0);
|
|
26
|
+
/** Page context from Chrome extension (URL, title, etc.) */
|
|
27
|
+
const extensionPageContext = useRef<any>(null);
|
|
28
|
+
/** Whether we're running inside the Chrome extension */
|
|
29
|
+
const isExtension = useRef(new URLSearchParams(location.search).has('ext'));
|
|
26
30
|
|
|
27
31
|
// Keep refs in sync with state
|
|
28
32
|
useEffect(() => { conversationIdRef.current = conversationId; }, [conversationId]);
|
|
29
33
|
useEffect(() => { streamingRef.current = streaming; }, [streaming]);
|
|
30
34
|
|
|
35
|
+
// Listen for page context from Chrome extension panel
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isExtension.current) return;
|
|
38
|
+
|
|
39
|
+
function handleExtMessage(event: MessageEvent) {
|
|
40
|
+
if (event.data?.type === 'bloby:page-context') {
|
|
41
|
+
extensionPageContext.current = event.data.context;
|
|
42
|
+
console.log('[blobyChat] Extension page context:', event.data.context?.url);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
window.addEventListener('message', handleExtMessage);
|
|
47
|
+
// Request initial context
|
|
48
|
+
window.parent?.postMessage({ type: 'bloby:request-context' }, '*');
|
|
49
|
+
|
|
50
|
+
return () => window.removeEventListener('message', handleExtMessage);
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
31
53
|
// Parse a raw DB message into a ChatMessage
|
|
32
54
|
const parseMessage = useCallback((m: any): ChatMessage => {
|
|
33
55
|
let audioData: string | undefined;
|
|
@@ -317,7 +339,24 @@ export function useBlobyChat(ws: WsClient | null, triggerReload?: number, enable
|
|
|
317
339
|
setMessages((msgs) => [...msgs, userMsg]);
|
|
318
340
|
}
|
|
319
341
|
|
|
320
|
-
|
|
342
|
+
// Prepend page context if running in Chrome extension
|
|
343
|
+
let messageContent = content;
|
|
344
|
+
if (isExtension.current && extensionPageContext.current) {
|
|
345
|
+
const ctx = extensionPageContext.current;
|
|
346
|
+
const parts = [`[Page: ${ctx.url}`];
|
|
347
|
+
if (ctx.title) parts[0] += ` | ${ctx.title}`;
|
|
348
|
+
if (ctx.productName) parts[0] += ` | ${ctx.productName}`;
|
|
349
|
+
if (ctx.price) parts[0] += ` | ${ctx.currency || '$'}${ctx.price}`;
|
|
350
|
+
parts[0] += ']';
|
|
351
|
+
if (ctx.selection) parts.push(`[Selected: ${ctx.selection}]`);
|
|
352
|
+
if (ctx.hasForms) parts.push(`[Page has ${ctx.formCount} form(s)]`);
|
|
353
|
+
messageContent = parts.join('\n') + '\n' + content;
|
|
354
|
+
|
|
355
|
+
// Request fresh context for next message
|
|
356
|
+
window.parent?.postMessage({ type: 'bloby:request-context' }, '*');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const payload: any = { conversationId, content: messageContent };
|
|
321
360
|
if (audioData) {
|
|
322
361
|
payload.audioData = audioData.includes(',') ? audioData.split(',')[1] : audioData;
|
|
323
362
|
}
|
package/supervisor/index.ts
CHANGED
|
@@ -80,7 +80,7 @@ const SW_JS = `// Service worker — app-shell caching + push notifications
|
|
|
80
80
|
// JS/CSS modules → stale-while-revalidate
|
|
81
81
|
// API, WebSocket, Vite internals → network-only (no cache)
|
|
82
82
|
|
|
83
|
-
var CACHE = 'bloby-
|
|
83
|
+
var CACHE = 'bloby-v10';
|
|
84
84
|
var HASHED_RE = new RegExp('/assets/.+-[a-zA-Z0-9]{6,}[.](js|css)$');
|
|
85
85
|
|
|
86
86
|
// Precache the HTML shell on install so the cache is never empty.
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
## Site-Specific Behaviors
|
|
105
|
+
|
|
106
|
+
When you detect the user is on a specific type of site (from the page context URL), adapt what you offer:
|
|
107
|
+
|
|
108
|
+
### YouTube (youtube.com, youtu.be)
|
|
109
|
+
- Offer to transcribe the video
|
|
110
|
+
- Offer to summarize, create bullet points, or extract key moments
|
|
111
|
+
- Offer to turn it into a blog post or audio summary
|
|
112
|
+
- The video URL is in the page context — you can fetch it or use tools to process it
|
|
113
|
+
|
|
114
|
+
### Shopping (amazon.com, ebay.com, walmart.com, etsy.com, etc.)
|
|
115
|
+
- Offer to find cheaper alternatives (web search)
|
|
116
|
+
- Offer to save to a wishlist (workspace file)
|
|
117
|
+
- Offer to compare prices across sites
|
|
118
|
+
- Product name and price are extracted from structured data when available
|
|
119
|
+
|
|
120
|
+
### Reddit / X / Social (reddit.com, x.com, twitter.com)
|
|
121
|
+
- Offer to summarize long threads
|
|
122
|
+
- Offer to draft a reply (the extension can type into input fields)
|
|
123
|
+
- Offer to save interesting posts to workspace notes
|
|
124
|
+
|
|
125
|
+
### Documentation / Articles (docs.*, medium.com, dev.to, any long-form text)
|
|
126
|
+
- Offer to summarize the article
|
|
127
|
+
- If text is selected, explain or elaborate on the selection
|
|
128
|
+
- Offer to save key points to workspace notes
|
|
129
|
+
|
|
130
|
+
### Forms (any page with detected form fields)
|
|
131
|
+
- When `hasForms` is true in context, offer to help fill the form
|
|
132
|
+
- Ask what information the user wants to fill
|
|
133
|
+
- Use the form field names from context to understand what's needed
|
|
134
|
+
|
|
135
|
+
### Email (gmail.com, outlook.com)
|
|
136
|
+
- Offer to draft replies
|
|
137
|
+
- Offer to summarize email threads
|
|
138
|
+
|
|
139
|
+
### Blocked Sites — NEVER interact with page content
|
|
140
|
+
- Banking: chase.com, wellsfargo.com, bankofamerica.com, any URL with /banking
|
|
141
|
+
- Auth pages: any URL with /login, /signin, /auth, /password
|
|
142
|
+
- Password managers: 1password.com, bitwarden.com, lastpass.com
|
|
143
|
+
- Medical records: mychart.com, any URL with /health-records
|
|
144
|
+
|
|
145
|
+
On blocked sites, you can still chat normally — just don't read or reference page content. If the user asks about the page, tell them: "I don't interact with sensitive pages like banking or login screens for your security."
|
|
146
|
+
|
|
147
|
+
### Default (any other site)
|
|
148
|
+
- Always share the URL and title (they're in the context)
|
|
149
|
+
- If the user asks about "this page" or "this", fetch the URL for details
|
|
150
|
+
- Offer to save interesting content to workspace
|
|
151
|
+
|
|
152
|
+
## Human Interaction
|
|
153
|
+
|
|
154
|
+
### What to tell your human
|
|
155
|
+
|
|
156
|
+
When they ask about the extension:
|
|
157
|
+
- "I can see whatever page you're on and help you with it"
|
|
158
|
+
- "Just click my bubble on any page and ask me anything"
|
|
159
|
+
- "If you highlight text on a page, I can see what you selected"
|
|
160
|
+
- "I can find better prices, summarize articles, save things to your workspace — whatever you need"
|
|
161
|
+
|
|
162
|
+
### What NOT to say
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
### Permissions
|
|
167
|
+
|
|
168
|
+
The extension only reads page content when your human actively opens the chat panel. It does NOT:
|
|
169
|
+
- Track browsing history
|
|
170
|
+
- Read pages in the background
|
|
171
|
+
- Send data anywhere except to your server
|
|
172
|
+
- Access passwords, form data, or cookies
|
|
173
|
+
|
|
174
|
+
If your human asks about privacy, be transparent about this.
|
|
175
|
+
|
|
176
|
+
## Notes
|
|
177
|
+
|
|
178
|
+
- 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.
|
|
179
|
+
- The pairing code is single-use and expires in 5 minutes. Generate a new one if it expires.
|
|
180
|
+
- The extension works on all pages except Chrome internal pages (`chrome://`, `chrome-extension://`) and your own Bloby dashboard domain (to avoid double-bubble).
|
|
181
|
+
- Multiple browsers/devices can be paired — each gets its own pairing code. They all share the same conversation.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bloby Chrome Extension — Background Service Worker
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Manages pairing state (serverUrl stored in chrome.storage.local)
|
|
6
|
+
* - Handles pairing flow (verify code → save config)
|
|
7
|
+
* - Responds to state queries from content scripts and popup
|
|
8
|
+
*
|
|
9
|
+
* Note: WebSocket connection is handled by the chat iframe inside the panel,
|
|
10
|
+
* not by the background worker. The iframe loads the Bloby chat app which
|
|
11
|
+
* manages its own WS connection and auth (portal login).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
let config = null; // { serverUrl, username, tier }
|
|
15
|
+
|
|
16
|
+
// ── Config Management ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
async function loadConfig() {
|
|
19
|
+
const data = await chrome.storage.local.get(['serverUrl', 'username', 'tier']);
|
|
20
|
+
if (data.serverUrl) {
|
|
21
|
+
config = data;
|
|
22
|
+
console.log(`[bloby-bg] Config loaded: ${config.serverUrl} (${config.username})`);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
console.log('[bloby-bg] No config found — not paired');
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function saveConfig(newConfig) {
|
|
30
|
+
config = newConfig;
|
|
31
|
+
await chrome.storage.local.set(newConfig);
|
|
32
|
+
console.log(`[bloby-bg] Config saved: ${config.serverUrl}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function clearConfig() {
|
|
36
|
+
config = null;
|
|
37
|
+
await chrome.storage.local.clear();
|
|
38
|
+
console.log('[bloby-bg] Config cleared — unpaired');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Broadcast to All Tabs ──────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
async function broadcastToTabs(msg) {
|
|
44
|
+
try {
|
|
45
|
+
const tabs = await chrome.tabs.query({});
|
|
46
|
+
for (const tab of tabs) {
|
|
47
|
+
if (tab.id) {
|
|
48
|
+
chrome.tabs.sendMessage(tab.id, msg).catch(() => {});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch { /* ignore */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Message Handler (from content scripts + popup) ─────────────────────────
|
|
55
|
+
|
|
56
|
+
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
57
|
+
if (msg.type === 'bloby:get-state') {
|
|
58
|
+
sendResponse({
|
|
59
|
+
paired: !!config?.serverUrl,
|
|
60
|
+
connected: true, // iframe manages its own connection
|
|
61
|
+
config: config ? { serverUrl: config.serverUrl, username: config.username } : null,
|
|
62
|
+
});
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (msg.type === 'bloby:pair') {
|
|
67
|
+
handlePair(msg.code).then(sendResponse);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Content script: take screenshot
|
|
72
|
+
if (msg.type === 'bloby:screenshot') {
|
|
73
|
+
chrome.tabs.captureVisibleTab(null, { format: 'png' }, (dataUrl) => {
|
|
74
|
+
sendResponse({ dataUrl: dataUrl || null });
|
|
75
|
+
});
|
|
76
|
+
return true; // async
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (msg.type === 'bloby:unpair') {
|
|
80
|
+
(async () => {
|
|
81
|
+
// Clear frame rules
|
|
82
|
+
try {
|
|
83
|
+
const existing = await chrome.declarativeNetRequest.getDynamicRules();
|
|
84
|
+
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
85
|
+
removeRuleIds: existing.map((r) => r.id),
|
|
86
|
+
});
|
|
87
|
+
} catch {}
|
|
88
|
+
await clearConfig();
|
|
89
|
+
broadcastToTabs({ type: 'bloby:unpaired' });
|
|
90
|
+
sendResponse({ success: true });
|
|
91
|
+
})();
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── Pairing Flow ───────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
async function handlePair(code) {
|
|
99
|
+
try {
|
|
100
|
+
console.log(`[bloby-bg] Verifying pairing code: ${code}`);
|
|
101
|
+
|
|
102
|
+
const res = await fetch('https://api.bloby.bot/api/extension/pair/verify', {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
body: JSON.stringify({ code }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const data = await res.json().catch(() => ({}));
|
|
110
|
+
console.log(`[bloby-bg] Pairing failed: ${data.error || res.status}`);
|
|
111
|
+
return { success: false, error: data.error || 'Invalid code' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const data = await res.json();
|
|
115
|
+
console.log(`[bloby-bg] Pairing verified: ${data.serverUrl}`);
|
|
116
|
+
|
|
117
|
+
await saveConfig({
|
|
118
|
+
serverUrl: data.serverUrl,
|
|
119
|
+
username: data.username,
|
|
120
|
+
tier: data.tier,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Set up header rules to allow iframing the Bloby server
|
|
124
|
+
await updateFrameRules(data.serverUrl);
|
|
125
|
+
|
|
126
|
+
// Notify all tabs to show the bubble
|
|
127
|
+
broadcastToTabs({ type: 'bloby:paired', serverUrl: data.serverUrl });
|
|
128
|
+
|
|
129
|
+
return { success: true, username: data.username, serverUrl: data.serverUrl };
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error('[bloby-bg] Pairing error:', err);
|
|
132
|
+
return { success: false, error: 'Network error — check your connection' };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Frame Header Rules ─────────────────────────────────────────────────────
|
|
137
|
+
// Strip X-Frame-Options and restrictive frame-ancestors from the Bloby server
|
|
138
|
+
// so the chat can be loaded in the extension's panel iframe.
|
|
139
|
+
|
|
140
|
+
async function updateFrameRules(serverUrl) {
|
|
141
|
+
const host = serverUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
142
|
+
console.log(`[bloby-bg] Setting frame rules for: *://${host}/*`);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Remove old rules
|
|
146
|
+
const existing = await chrome.declarativeNetRequest.getDynamicRules();
|
|
147
|
+
const removeIds = existing.map((r) => r.id);
|
|
148
|
+
|
|
149
|
+
await chrome.declarativeNetRequest.updateDynamicRules({
|
|
150
|
+
removeRuleIds: removeIds,
|
|
151
|
+
addRules: [
|
|
152
|
+
{
|
|
153
|
+
id: 1,
|
|
154
|
+
priority: 1,
|
|
155
|
+
action: {
|
|
156
|
+
type: 'modifyHeaders',
|
|
157
|
+
responseHeaders: [
|
|
158
|
+
{ header: 'X-Frame-Options', operation: 'remove' },
|
|
159
|
+
{ header: 'Content-Security-Policy', operation: 'remove' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
condition: {
|
|
163
|
+
urlFilter: `*://${host}/*`,
|
|
164
|
+
resourceTypes: ['sub_frame'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
console.log('[bloby-bg] Frame rules set successfully');
|
|
170
|
+
} catch (err) {
|
|
171
|
+
console.error('[bloby-bg] Failed to set frame rules:', err);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Startup ────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
(async () => {
|
|
178
|
+
const hasCfg = await loadConfig();
|
|
179
|
+
if (hasCfg && config.serverUrl) {
|
|
180
|
+
await updateFrameRules(config.serverUrl);
|
|
181
|
+
}
|
|
182
|
+
})();
|