Add development plan with 13 milestone specifications
- 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
This commit is contained in:
339
docs/milestones/12-browser-extension.md
Normal file
339
docs/milestones/12-browser-extension.md
Normal file
@@ -0,0 +1,339 @@
|
||||
# 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)
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```html
|
||||
<!-- 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
|
||||
|
||||
```javascript
|
||||
// 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/readability` for content extraction
|
||||
- Chrome/Edge extension APIs
|
||||
|
||||
## References
|
||||
|
||||
- [Chrome Extension Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/)
|
||||
- [Native Messaging](https://developer.chrome.com/docs/extensions/mv3/nativeMessaging/)
|
||||
- [Readability](https://github.com/mozilla/readability)
|
||||
Reference in New Issue
Block a user