+
Save to Cortex
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/extension/popup/popup.js b/extension/popup/popup.js
new file mode 100644
index 0000000..ade3e8b
--- /dev/null
+++ b/extension/popup/popup.js
@@ -0,0 +1,136 @@
+// Popup script for Cortex browser extension
+
+const CORTEX_API = 'http://localhost:3100/api';
+
+// DOM elements
+const titleInput = document.getElementById('title');
+const kindSelect = document.getElementById('kind');
+const tagsInput = document.getElementById('tags');
+const includeContent = document.getElementById('includeContent');
+const selectionOnly = document.getElementById('selectionOnly');
+const saveButton = document.getElementById('save');
+const cancelButton = document.getElementById('cancel');
+const statusDiv = document.getElementById('status');
+
+let pageData = null;
+
+// Initialize popup
+async function init() {
+ try {
+ // Get current tab
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+
+ // Request content from content script
+ const response = await chrome.tabs.sendMessage(tab.id, { action: 'extract' });
+ pageData = response;
+
+ // Pre-fill form
+ titleInput.value = response.title || tab.title || '';
+
+ // Suggest tags based on URL
+ const url = new URL(tab.url);
+ const suggestedTags = ['web-clip'];
+ if (url.hostname.includes('github')) suggestedTags.push('github');
+ if (url.hostname.includes('docs')) suggestedTags.push('documentation');
+ if (url.hostname.includes('stackoverflow')) suggestedTags.push('stackoverflow');
+ tagsInput.value = suggestedTags.join(', ');
+
+ // Enable selection only if there's selected text
+ if (response.selection) {
+ selectionOnly.disabled = false;
+ } else {
+ selectionOnly.disabled = true;
+ }
+
+ } catch (err) {
+ showStatus('Could not extract page content', 'error');
+ console.error('Init error:', err);
+ }
+}
+
+// Save to Cortex
+async function save() {
+ saveButton.disabled = true;
+ saveButton.textContent = 'Saving...';
+
+ try {
+ const content = selectionOnly.checked
+ ? pageData?.selection
+ : (includeContent.checked ? pageData?.content : '');
+
+ const tags = tagsInput.value
+ .split(',')
+ .map(t => t.trim())
+ .filter(t => t.length > 0);
+
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
+
+ const response = await fetch(`${CORTEX_API}/nodes`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ kind: kindSelect.value,
+ title: titleInput.value,
+ content: content,
+ tags: tags,
+ metadata: {
+ source: {
+ type: 'url',
+ url: tab.url,
+ savedAt: Date.now(),
+ },
+ },
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+
+ const node = await response.json();
+ showStatus(`Saved! ID: ${node.id.slice(0, 8)}`, 'success');
+
+ // Close popup after brief delay
+ setTimeout(() => window.close(), 1500);
+
+ } catch (err) {
+ showStatus(`Failed to save: ${err.message}`, 'error');
+ console.error('Save error:', err);
+
+ // Check if Cortex server is running
+ if (err.message.includes('fetch')) {
+ showStatus('Is Cortex server running? (cortex serve)', 'error');
+ }
+ } finally {
+ saveButton.disabled = false;
+ saveButton.textContent = 'Save';
+ }
+}
+
+// Show status message
+function showStatus(message, type) {
+ statusDiv.textContent = message;
+ statusDiv.className = `status ${type}`;
+}
+
+// Event listeners
+saveButton.addEventListener('click', save);
+cancelButton.addEventListener('click', () => window.close());
+
+// Toggle selection only checkbox when include content is unchecked
+includeContent.addEventListener('change', () => {
+ if (!includeContent.checked) {
+ selectionOnly.checked = false;
+ }
+});
+
+selectionOnly.addEventListener('change', () => {
+ if (selectionOnly.checked) {
+ includeContent.checked = true;
+ }
+});
+
+// Initialize on load
+init();
diff --git a/src/cli/commands/completions.ts b/src/cli/commands/completions.ts
new file mode 100644
index 0000000..5be5707
--- /dev/null
+++ b/src/cli/commands/completions.ts
@@ -0,0 +1,169 @@
+import { Command } from 'commander';
+import chalk from 'chalk';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+import { generateCompletions, Shell } from '../completions';
+import { listNodes } from '../../core/store';
+import { listGraphs } from '../../core/graphs';
+import { getDb } from '../../core/db';
+
+export const completionsCommand = new Command('completions')
+ .description('Generate shell completions')
+ .argument('[shell]', 'Shell type: bash, zsh, fish, powershell')
+ .option('--install', 'Install completions to appropriate location')
+ .action(async (shell: string | undefined, opts) => {
+ const validShells: Shell[] = ['bash', 'zsh', 'fish', 'powershell'];
+
+ if (!shell) {
+ console.log(chalk.cyan('Available shell completions:\n'));
+ console.log(' cortex completions bash');
+ console.log(' cortex completions zsh');
+ console.log(' cortex completions fish');
+ console.log(' cortex completions powershell');
+ console.log(chalk.dim('\nAdd --install to auto-install for your shell'));
+ return;
+ }
+
+ const shellType = shell.toLowerCase() as Shell;
+ if (!validShells.includes(shellType)) {
+ console.error(chalk.red(`Invalid shell: ${shell}`));
+ console.log(chalk.dim(`Valid shells: ${validShells.join(', ')}`));
+ process.exit(1);
+ }
+
+ const completionScript = generateCompletions(shellType);
+
+ if (opts.install) {
+ installCompletions(shellType, completionScript);
+ } else {
+ console.log(completionScript);
+ }
+ });
+
+function installCompletions(shell: Shell, script: string): void {
+ const home = os.homedir();
+
+ switch (shell) {
+ case 'bash': {
+ const bashrc = path.join(home, '.bashrc');
+ const completionMarker = '# Cortex CLI completions';
+
+ // Check if already installed
+ if (fs.existsSync(bashrc)) {
+ const content = fs.readFileSync(bashrc, 'utf-8');
+ if (content.includes(completionMarker)) {
+ console.log(chalk.yellow('Completions already installed in ~/.bashrc'));
+ return;
+ }
+ }
+
+ fs.appendFileSync(bashrc, `\n${completionMarker}\n${script}\n`);
+ console.log(chalk.green('✓ Installed bash completions to ~/.bashrc'));
+ console.log(chalk.dim(' Run: source ~/.bashrc'));
+ break;
+ }
+
+ case 'zsh': {
+ const zshCompletions = path.join(home, '.zsh', 'completions');
+ const targetFile = path.join(zshCompletions, '_cortex');
+
+ fs.mkdirSync(zshCompletions, { recursive: true });
+ fs.writeFileSync(targetFile, script);
+
+ console.log(chalk.green(`✓ Installed zsh completions to ${targetFile}`));
+ console.log(chalk.dim(' Add to ~/.zshrc: fpath=(~/.zsh/completions $fpath)'));
+ console.log(chalk.dim(' Then run: autoload -Uz compinit && compinit'));
+ break;
+ }
+
+ case 'fish': {
+ const fishCompletions = path.join(home, '.config', 'fish', 'completions');
+ const targetFile = path.join(fishCompletions, 'cortex.fish');
+
+ fs.mkdirSync(fishCompletions, { recursive: true });
+ fs.writeFileSync(targetFile, script);
+
+ console.log(chalk.green(`✓ Installed fish completions to ${targetFile}`));
+ console.log(chalk.dim(' Completions will be loaded automatically'));
+ break;
+ }
+
+ case 'powershell': {
+ // PowerShell profile varies by platform
+ const profilePaths = [
+ path.join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1'),
+ path.join(home, 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1'),
+ path.join(home, '.config', 'powershell', 'Microsoft.PowerShell_profile.ps1'),
+ ];
+
+ let profilePath = profilePaths.find(p => fs.existsSync(path.dirname(p)));
+ if (!profilePath) {
+ profilePath = profilePaths[0];
+ fs.mkdirSync(path.dirname(profilePath), { recursive: true });
+ }
+
+ const marker = '# Cortex CLI completions';
+ if (fs.existsSync(profilePath)) {
+ const content = fs.readFileSync(profilePath, 'utf-8');
+ if (content.includes(marker)) {
+ console.log(chalk.yellow(`Completions already installed in ${profilePath}`));
+ return;
+ }
+ }
+
+ fs.appendFileSync(profilePath, `\n${marker}\n${script}\n`);
+ console.log(chalk.green(`✓ Installed PowerShell completions to ${profilePath}`));
+ console.log(chalk.dim(' Restart PowerShell to load completions'));
+ break;
+ }
+ }
+}
+
+// Hidden helper commands for dynamic completions
+export const getNodesCommand = new Command('--get-nodes')
+ .argument('[prefix]', 'Node ID prefix to filter')
+ .description('Helper for shell completions')
+ .action(async (prefix?: string) => {
+ try {
+ const nodes = listNodes({ limit: 30, includeStale: false });
+ const filtered = prefix
+ ? nodes.filter(n => n.id.startsWith(prefix) || n.title.toLowerCase().includes(prefix.toLowerCase()))
+ : nodes;
+
+ // Output tab-separated: id