Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions integrations/chrome-extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Chrome Extension — Browser Capture & Search

Save and search your Open Brain second brain directly from your browser. Capture thoughts from any webpage with automatic source tracking, search semantically across all your thoughts, and use keyboard shortcuts for fast access.

## Features

- **Save thoughts** from any webpage — source URL and page title are attached automatically
- **Semantic search** across all your thoughts (browser, Telegram, Slack, Claude)
- **Auto-capture** — select text on a page, open the extension, and it's pre-filled
- **Right-click menu** — save or search selected text without opening the popup
- **Omnibox** — type `brain <query>` in the address bar for instant search
- **Related thoughts** — after saving, see similar thoughts you captured before
- **Source filter** — filter search results by origin (browser, Telegram, Slack, Claude)
- **Click to copy** — click any search result to copy it to clipboard
- **Delete and complete** — remove thoughts or mark tasks as done directly from search results
- **Keyboard shortcut** — `Ctrl+Shift+B` (or `Cmd+Shift+B` on Mac) to open
- **Stats bar** — see total thoughts, today's count, and this week's count at a glance

## Prerequisites

- A working [Open Brain](https://github.com/NateBJones-Projects/OB1) setup with:
- The `thoughts` table and `match_thoughts` function in Supabase
- An [OpenRouter](https://openrouter.ai) API key (for embeddings and metadata extraction)
- Chrome or any Chromium-based browser (Edge, Brave, Arc, etc.)
- Supabase CLI installed (`npm install -g supabase`)

## Step-by-Step Setup

### 1. Deploy the brain-api Edge Function

The extension communicates with your Open Brain through a lightweight REST API (included in `supabase-function/`).

**Set your secrets** (one-time):

```bash
supabase secrets set OPENROUTER_API_KEY=your-openrouter-key --project-ref YOUR_PROJECT_REF
supabase secrets set BRAIN_API_KEY=$(openssl rand -hex 32) --project-ref YOUR_PROJECT_REF
```

Write down the `BRAIN_API_KEY` you generated — you will need it in step 3.

**Deploy the function:**

```bash
cd supabase-function
supabase functions deploy brain-api --project-ref YOUR_PROJECT_REF --no-verify-jwt
```

**Verify it works:**

```bash
curl -X POST https://YOUR_PROJECT_REF.supabase.co/functions/v1/brain-api \
-H "Content-Type: application/json" \
-H "x-brain-key: YOUR_BRAIN_API_KEY" \
-d '{"action": "stats"}'
```

You should see a JSON response with `total`, `today`, and `this_week` counts.

### 2. Install the Chrome Extension

1. Open `chrome://extensions/` in your browser
2. Enable **Developer mode** (toggle in the top right)
3. Click **Load unpacked**
4. Navigate to the `chrome-extension/` folder inside this contribution and select it
5. The Open Brain icon (brain emoji) should now appear in your toolbar

### 3. Configure the Extension

1. Click the Open Brain icon in your toolbar (or press `Ctrl+Shift+B`)
2. The settings panel opens automatically on first use
3. Enter:
- **API URL**: `https://YOUR_PROJECT_REF.supabase.co/functions/v1/brain-api`
- **API Key**: the `BRAIN_API_KEY` you generated in step 1
4. Click **Einstellungen speichern**

The stats bar should now show your thought counts.

## Expected Outcome

After setup, you should be able to:

1. **Open the popup** with `Ctrl+Shift+B` and see your thought stats (total, today, this week)
2. **Type a thought** in the text area and press `Ctrl+Enter` to save it — you should see "Gedanke gespeichert!" and related thoughts appear below
3. **Search** by typing a query in the search field — results appear with source badges, dates, and similarity scores
4. **Right-click** selected text on any webpage and see "Im Brain speichern" and "Im Brain suchen" in the context menu
5. **Type `brain` in the address bar**, press Tab, and type a query to get instant search suggestions

## Usage

| Action | How |
| --- | --- |
| Save a thought | Open popup, type, press `Ctrl+Enter` |
| Save selected text | Select text on page, right-click, "Im Brain speichern" |
| Search | Open popup, type in search field, press Enter |
| Search selected text | Select text, right-click, "Im Brain suchen" |
| Omnibox search | Type `brain` in address bar, press Tab, type query |
| Copy a result | Click on the result text |
| Delete a thought | Hover over result, click X, confirm |
| Complete a task | Hover over a task result, click "Erledigt" |
| Filter by source | Use the dropdown next to the search field |

## Troubleshooting

**"API nicht konfiguriert" error**
Open the extension, expand Settings at the bottom, and enter your API URL and API Key. Make sure there are no trailing spaces.

**Stats show "--" after configuration**
Your brain-api function may not be deployed or the API key is wrong. Test with the curl command from step 1 above.

**Right-click menu doesn't appear**
Go to `chrome://extensions/`, find Open Brain, and click the reload button. The context menu is registered on install — reloading the extension re-triggers it.

**"Suche laeuft..." hangs forever**
Check that your Supabase project is active (not paused). Free-tier projects pause after 7 days of inactivity. Go to your Supabase dashboard and restore it if needed.

**Extension doesn't capture selected text automatically**
Auto-capture doesn't work on `chrome://` pages, PDF viewers, or pages with strict Content Security Policy. This is a Chrome security restriction.

## Tech Stack

- Chrome Extension Manifest V3
- Supabase Edge Functions (Deno/TypeScript)
- pgvector for semantic search
- OpenRouter API (text-embedding-3-small for embeddings, gpt-4o-mini for metadata extraction)

## License

MIT
203 changes: 203 additions & 0 deletions integrations/chrome-extension/chrome-extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// --- Context Menu Setup ---

chrome.runtime.onInstalled.addListener(() => {
// Context menu: save selected text to Brain
chrome.contextMenus.create({
id: "save-to-brain",
title: "Save to Brain",
contexts: ["selection"],
});

// Context menu: search selected text in Brain
chrome.contextMenus.create({
id: "search-in-brain",
title: "Search in Brain",
contexts: ["selection"],
});
});

// --- Context Menu Handler ---

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
const selectedText = info.selectionText;
if (!selectedText) return;

// --- "Search in Brain" ---
if (info.menuItemId === "search-in-brain") {
// Store the search query so the popup can pick it up
await chrome.storage.local.set({ pendingSearch: selectedText });

// Try to open the popup programmatically (requires Chrome 99+)
try {
await chrome.action.openPopup();
} catch (err) {
// openPopup() not available or failed -- the popup will pick up
// pendingSearch on next manual open
console.log("[Open Brain] openPopup not available, pendingSearch set:", err);
}
return;
}

// --- "Save to Brain" ---
if (info.menuItemId !== "save-to-brain") return;

const pageUrl = tab?.url || "unknown";
const pageTitle = tab?.title || "Unknown page";

// Format: content + source context
const content = `${selectedText}\n\n(Source: ${pageTitle} \u2014 ${pageUrl})`;

try {
const { apiUrl, apiKey } = await chrome.storage.sync.get([
"apiUrl",
"apiKey",
]);

if (!apiUrl || !apiKey) {
showNotification(
"Configuration missing",
"Please configure the API URL and API Key in settings.",
false
);
return;
}

const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-brain-key": apiKey,
},
body: JSON.stringify({
action: "save",
content: content,
metadata: {
source: "browser",
url: pageUrl,
title: pageTitle,
},
}),
});

if (!response.ok) {
const text = await response.text();
throw new Error(`API error (${response.status}): ${text}`);
}

showNotification(
"Thought saved",
`"${truncate(selectedText, 80)}" has been saved to Brain.`,
true
);
} catch (err) {
console.error("Open Brain - Error saving:", err);
showNotification(
"Error saving",
err.message || "Unknown error",
false
);
}
});

// --- Omnibox (address bar: "brain <query>") ---

chrome.omnibox.onInputStarted.addListener(() => {
chrome.omnibox.setDefaultSuggestion({
description: "Search Brain: %s",
});
});

chrome.omnibox.onInputChanged.addListener(async (text, suggest) => {
const query = text.trim();
if (!query || query.length < 2) {
suggest([]);
return;
}

try {
const { apiUrl, apiKey } = await chrome.storage.sync.get(["apiUrl", "apiKey"]);
if (!apiUrl || !apiKey) {
suggest([]);
return;
}

const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-brain-key": apiKey,
},
body: JSON.stringify({ action: "search", query: query }),
});

if (!response.ok) {
suggest([]);
return;
}

const data = await response.json();
const results = data.results || data.thoughts || [];

const suggestions = results.slice(0, 5).map((item) => {
const content = item.content || item.thought || item.text || "";
// Omnibox description supports XML: escape special chars
const desc = escapeXml(truncate(content, 200));
// content field is used when suggestion is selected
return {
content: content,
description: desc,
};
});

suggest(suggestions);
} catch (err) {
console.error("[Open Brain] Omnibox search failed:", err);
suggest([]);
}
});

chrome.omnibox.onInputEntered.addListener(async (text, disposition) => {
// text is either the typed query (default suggestion) or the selected suggestion content
// Store as pendingSearch so popup can pick it up, then try to open popup
await chrome.storage.local.set({ pendingSearch: text });

try {
await chrome.action.openPopup();
} catch (err) {
// If openPopup fails, copy to clipboard via offscreen or just log
console.log("[Open Brain] openPopup not available after omnibox selection:", err);
}
});

// --- Notification Helper ---

function showNotification(title, message, success) {
// Use the badge as a quick visual indicator
const badgeText = success ? "OK" : "ERR";
const badgeColor = success ? "#4caf50" : "#ef5350";

chrome.action.setBadgeText({ text: badgeText });
chrome.action.setBadgeBackgroundColor({ color: badgeColor });

// Clear badge after 3 seconds
setTimeout(() => {
chrome.action.setBadgeText({ text: "" });
}, 3000);

// Log for debugging
console.log(`[Open Brain] ${title}: ${message}`);
}

function truncate(str, maxLen) {
if (str.length <= maxLen) return str;
return str.substring(0, maxLen - 3) + "...";
}

function escapeXml(str) {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions integrations/chrome-extension/chrome-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"manifest_version": 3,
"name": "Open Brain",
"version": "1.0.0",
"description": "Save & search your second brain from any webpage",
"permissions": [
"contextMenus",
"storage",
"activeTab",
"scripting"
],
"host_permissions": [
"https://*.supabase.co/*"
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"default_title": "Open Brain"
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"background": {
"service_worker": "background.js"
},
"omnibox": {
"keyword": "brain"
},
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+Shift+B",
"mac": "Command+Shift+B"
},
"description": "Open Brain Popup"
}
}
}
Loading