Add browser extension and shell completions (Milestones 12-13)

M12: Browser Extension
- Chrome/Edge Manifest V3 extension
- Popup UI for saving pages
- Context menu integration (save page/selection/link)
- Background service worker
- Content script for extraction

M13: Shell Completions
- Bash, Zsh, Fish, PowerShell completions
- Dynamic node ID completion
- Dynamic tag completion
- Dynamic graph completion
- Auto-install command (--install)
This commit is contained in:
2026-02-03 11:35:41 +01:00
parent b1c62c5da9
commit f21426fc43
11 changed files with 1248 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
// Background service worker for Cortex browser extension
const CORTEX_API = 'http://localhost:3100/api';
// Create context menu on install
chrome.runtime.onInstalled.addListener(() => {
// Save page context menu
chrome.contextMenus.create({
id: 'cortex-save-page',
title: 'Save page to Cortex',
contexts: ['page'],
});
// Save selection context menu
chrome.contextMenus.create({
id: 'cortex-save-selection',
title: 'Save selection to Cortex',
contexts: ['selection'],
});
// Save link context menu
chrome.contextMenus.create({
id: 'cortex-save-link',
title: 'Save link to Cortex',
contexts: ['link'],
});
});
// Handle context menu clicks
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
try {
let content = '';
let title = tab.title;
if (info.menuItemId === 'cortex-save-selection') {
content = info.selectionText || '';
title = `Selection from: ${tab.title}`;
} else if (info.menuItemId === 'cortex-save-link') {
content = `Link: ${info.linkUrl}`;
title = info.linkUrl;
} else {
// Get full page content
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: extractPageContent,
});
content = result?.content || '';
title = result?.title || tab.title;
}
// Save to Cortex
await saveToCortex({
title,
content,
url: tab.url,
kind: 'memory',
tags: ['web-clip'],
});
// Show notification (if supported)
if (chrome.notifications) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon-48.png',
title: 'Saved to Cortex',
message: title.slice(0, 50),
});
}
} catch (err) {
console.error('Context menu action failed:', err);
}
});
// Function to extract page content (injected into page)
function extractPageContent() {
// Simple content extraction
// A full implementation would use Readability
const title = document.title;
// Try to get main content
const article = document.querySelector('article');
const main = document.querySelector('main');
const content = article?.textContent || main?.textContent || document.body.textContent || '';
// Clean up content
const cleanContent = content
.replace(/\s+/g, ' ')
.replace(/\n+/g, '\n')
.trim()
.slice(0, 50000); // Limit content size
return {
title,
content: cleanContent,
url: window.location.href,
};
}
// Save to Cortex API
async function saveToCortex(data) {
const response = await fetch(`${CORTEX_API}/nodes`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
kind: data.kind || 'memory',
title: data.title,
content: data.content,
tags: data.tags || ['web-clip'],
metadata: {
source: {
type: 'url',
url: data.url,
savedAt: Date.now(),
},
},
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
// Listen for messages from content script or popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'save') {
saveToCortex(request.data)
.then(node => sendResponse({ success: true, nodeId: node.id }))
.catch(err => sendResponse({ success: false, error: err.message }));
return true; // Keep channel open for async response
}
});