- docs/plan.md: Master roadmap with phases and priorities - docs/milestones/01-13: Detailed specs for each feature - Updated CLAUDE.md with plan references and build commands Milestones cover: - Phase 1: Temporal versioning, auto-capture, context injection, codebase indexing - Phase 2: Daily journal, content ingestion, graph visualization, import/export - Phase 3: Multi-graph, smart retrieval, TUI dashboard, browser extension, shell completions
8.3 KiB
8.3 KiB
Milestone 12: Browser Extension
Overview
Browser extension to save content from any webpage directly to Cortex. One-click capture for research and documentation.
Motivation
- Web research is a major knowledge source
- Manual copy-paste is friction
- Supermemory's browser extension is popular
- Capture context while browsing
Features
12.1 Quick Save
Right-click any page → "Save to Cortex"
12.2 Selection Save
Select text → Right-click → "Save selection to Cortex"
12.3 Popup Interface
┌─────────────────────────────────────────┐
│ 🧠 Save to Cortex │
├─────────────────────────────────────────┤
│ Title: [API Documentation ] │
│ Kind: [memory ▼] │
│ Tags: [docs, api, reference ] │
│ │
│ ☑ Include page content │
│ ☐ Include selection only │
│ ☐ Include screenshot │
│ │
│ [Save] [Cancel] │
└─────────────────────────────────────────┘
12.4 Auto-Extract
Automatically extract:
- Page title
- Main content (via Readability)
- Meta description
- Author/date if available
12.5 Tag Suggestions
Suggest tags based on:
- Page content analysis
- Existing tags in Cortex
- URL domain
12.6 Local Communication
Extension communicates with local Cortex server:
- Native messaging (preferred)
- Local HTTP API fallback
Implementation
Extension Structure
extension/
├── manifest.json
├── popup/
│ ├── popup.html
│ ├── popup.js
│ └── popup.css
├── content/
│ └── content.js # Injected into pages
├── background/
│ └── background.js # Service worker
├── icons/
│ ├── icon-16.png
│ ├── icon-48.png
│ └── icon-128.png
└── native/
└── cortex-native.js # Native messaging host
Manifest (v3)
{
"manifest_version": 3,
"name": "Cortex - Save to Memory",
"version": "1.0.0",
"description": "Save web content to your Cortex knowledge graph",
"permissions": [
"activeTab",
"contextMenus",
"storage",
"nativeMessaging"
],
"host_permissions": [
"http://localhost:3100/*"
],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"background": {
"service_worker": "background/background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content.js"]
}
]
}
Content Script
// content/content.js
// Extract page content using Readability
function extractContent() {
const clone = document.cloneNode(true);
const reader = new Readability(clone);
const article = reader.parse();
return {
title: article?.title || document.title,
content: article?.textContent || '',
excerpt: article?.excerpt || '',
url: window.location.href,
selection: window.getSelection()?.toString() || '',
};
}
// Listen for messages from popup/background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'extract') {
sendResponse(extractContent());
}
});
Background Script
// background/background.js
// Context menu
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'save-to-cortex',
title: 'Save to Cortex',
contexts: ['page', 'selection'],
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === 'save-to-cortex') {
// Get content from page
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.__cortexExtract(),
});
// Save via native messaging or HTTP
await saveToCortext(result);
}
});
async function saveToCortex(content) {
try {
// Try native messaging first
const response = await chrome.runtime.sendNativeMessage('cortex', {
action: 'save',
data: content,
});
return response;
} catch {
// Fall back to HTTP API
const response = await fetch('http://localhost:3100/api/nodes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kind: 'memory',
title: content.title,
content: content.content,
tags: ['web-clip'],
metadata: {
source: { type: 'url', url: content.url },
},
}),
});
return response.json();
}
}
Popup UI
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="container">
<h1>🧠 Save to Cortex</h1>
<div class="field">
<label>Title</label>
<input type="text" id="title" />
</div>
<div class="field">
<label>Kind</label>
<select id="kind">
<option value="memory">Memory</option>
<option value="component">Component</option>
<option value="decision">Decision</option>
<option value="task">Task</option>
</select>
</div>
<div class="field">
<label>Tags</label>
<input type="text" id="tags" placeholder="comma, separated" />
</div>
<div class="field">
<label>
<input type="checkbox" id="includeContent" checked />
Include page content
</label>
</div>
<div class="actions">
<button id="save">Save</button>
<button id="cancel">Cancel</button>
</div>
</div>
<script src="popup.js"></script>
</body>
</html>
Native Messaging Host
// native/cortex-native.js
#!/usr/bin/env node
const { addNode } = require('../dist/core/store');
process.stdin.on('readable', () => {
// Read message length (4 bytes)
const lengthBuf = process.stdin.read(4);
if (!lengthBuf) return;
const length = lengthBuf.readUInt32LE(0);
const messageBuf = process.stdin.read(length);
const message = JSON.parse(messageBuf.toString());
handleMessage(message).then(response => {
const responseBuf = Buffer.from(JSON.stringify(response));
const lengthBuf = Buffer.alloc(4);
lengthBuf.writeUInt32LE(responseBuf.length, 0);
process.stdout.write(lengthBuf);
process.stdout.write(responseBuf);
});
});
async function handleMessage(message) {
if (message.action === 'save') {
const node = await addNode({
kind: message.data.kind || 'memory',
title: message.data.title,
content: message.data.content,
tags: message.data.tags || ['web-clip'],
metadata: { source: message.data.source },
});
return { success: true, nodeId: node.id };
}
return { success: false, error: 'Unknown action' };
}
CLI Commands
| Command | Description |
|---|---|
cortex extension install |
Install native messaging host |
cortex extension status |
Check extension connectivity |
Testing
- Extension installs in Chrome/Edge
- Context menu appears
- Popup opens and pre-fills data
- Save creates node in Cortex
- Selection save captures selected text
- Native messaging works
- HTTP fallback works
Acceptance Criteria
- One-click save from any webpage
- Content extracted cleanly
- Tags can be added before save
- Works without Cortex server running (queues)
- Firefox + Chrome support
Estimated Effort
- Extension scaffold: 2 hours
- Content extraction: 3 hours
- Popup UI: 4 hours
- Background worker: 3 hours
- Native messaging: 4 hours
- Testing/polish: 4 hours
- Total: ~20 hours
Dependencies
@anthropic/readabilityfor content extraction- Chrome/Edge extension APIs