diff --git a/extension/README.md b/extension/README.md new file mode 100644 index 0000000..26a0ba1 --- /dev/null +++ b/extension/README.md @@ -0,0 +1,75 @@ +# Cortex Browser Extension + +Save web content directly to your Cortex knowledge graph. + +## Features + +- One-click save from any webpage +- Right-click context menu integration +- Save selected text only +- Auto-extract page title and content +- Tag suggestions based on URL + +## Installation + +### Chrome / Edge + +1. Open `chrome://extensions` (or `edge://extensions`) +2. Enable "Developer mode" in the top right +3. Click "Load unpacked" +4. Select this `extension` folder + +### Firefox + +1. Open `about:debugging` +2. Click "This Firefox" +3. Click "Load Temporary Add-on" +4. Select `manifest.json` from this folder + +## Usage + +### Popup + +1. Click the Cortex icon in your browser toolbar +2. Edit the title and tags as needed +3. Choose whether to include page content +4. Click "Save" + +### Context Menu + +- **Save page**: Right-click anywhere on a page → "Save page to Cortex" +- **Save selection**: Select text, right-click → "Save selection to Cortex" +- **Save link**: Right-click a link → "Save link to Cortex" + +## Requirements + +The Cortex server must be running for the extension to work: + +```bash +cortex serve +``` + +By default, the extension connects to `http://localhost:3100/api`. + +## Development + +The extension uses Manifest V3 and consists of: + +- `manifest.json` - Extension configuration +- `popup/` - Popup UI (HTML, CSS, JS) +- `background/` - Service worker for context menus +- `content/` - Content script for page extraction +- `icons/` - Extension icons + +## Icon Generation + +To generate PNG icons from the SVG: + +```bash +# Using ImageMagick +convert icons/icon.svg -resize 16x16 icons/icon-16.png +convert icons/icon.svg -resize 48x48 icons/icon-48.png +convert icons/icon.svg -resize 128x128 icons/icon-128.png +``` + +Or use an online SVG to PNG converter. diff --git a/extension/background/background.js b/extension/background/background.js new file mode 100644 index 0000000..8e2eec3 --- /dev/null +++ b/extension/background/background.js @@ -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 + } +}); diff --git a/extension/content/content.js b/extension/content/content.js new file mode 100644 index 0000000..51e9e01 --- /dev/null +++ b/extension/content/content.js @@ -0,0 +1,80 @@ +// Content script for Cortex browser extension +// Injected into web pages to extract content + +// Extract page content +function extractContent() { + const title = document.title; + + // Get selected text + const selection = window.getSelection()?.toString() || ''; + + // Try to find main content using common selectors + let content = ''; + const contentSelectors = [ + 'article', + '[role="main"]', + 'main', + '.post-content', + '.article-content', + '.entry-content', + '.content', + '#content', + ]; + + for (const selector of contentSelectors) { + const el = document.querySelector(selector); + if (el && el.textContent) { + content = el.textContent; + break; + } + } + + // Fall back to body content + if (!content) { + content = document.body.textContent || ''; + } + + // Clean up content + content = content + .replace(/\s+/g, ' ') + .replace(/\n{3,}/g, '\n\n') + .trim() + .slice(0, 100000); // Limit to 100k chars + + // Get meta description + const metaDesc = document.querySelector('meta[name="description"]')?.content || ''; + + // Get author + const author = document.querySelector('meta[name="author"]')?.content || + document.querySelector('[rel="author"]')?.textContent || ''; + + // Get published date + const datePublished = document.querySelector('meta[property="article:published_time"]')?.content || + document.querySelector('time[datetime]')?.getAttribute('datetime') || ''; + + return { + title, + content, + selection, + excerpt: metaDesc || content.slice(0, 200), + url: window.location.href, + author, + datePublished, + }; +} + +// Listen for messages from popup or background +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'extract') { + try { + const data = extractContent(); + sendResponse(data); + } catch (err) { + sendResponse({ error: err.message }); + } + } + return true; // Keep channel open for async response +}); + +// Make extract function available globally for background script injection +window.__cortexExtract = extractContent; diff --git a/extension/icons/icon.svg b/extension/icons/icon.svg new file mode 100644 index 0000000..7f0ec33 --- /dev/null +++ b/extension/icons/icon.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 0000000..d079d5c --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,36 @@ +{ + "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" + ], + "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": [""], + "js": ["content/content.js"] + } + ], + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } +} diff --git a/extension/popup/popup.css b/extension/popup/popup.css new file mode 100644 index 0000000..e900772 --- /dev/null +++ b/extension/popup/popup.css @@ -0,0 +1,136 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + width: 320px; + padding: 16px; + background: #1a1a2e; + color: #eee; +} + +.container { + display: flex; + flex-direction: column; + gap: 12px; +} + +h1 { + font-size: 18px; + font-weight: 600; + color: #4fc3f7; + margin-bottom: 8px; +} + +.status { + padding: 8px; + border-radius: 4px; + font-size: 12px; + display: none; +} + +.status.success { + display: block; + background: #1b5e20; + color: #a5d6a7; +} + +.status.error { + display: block; + background: #b71c1c; + color: #ef9a9a; +} + +.field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.field.checkbox { + flex-direction: row; + align-items: center; +} + +.field.checkbox label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; +} + +label { + font-size: 12px; + color: #aaa; + font-weight: 500; +} + +input[type="text"], +select { + padding: 8px 12px; + border: 1px solid #333; + border-radius: 4px; + background: #0f0f23; + color: #eee; + font-size: 14px; +} + +input[type="text"]:focus, +select:focus { + outline: none; + border-color: #4fc3f7; +} + +select { + cursor: pointer; +} + +input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; +} + +.actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +button { + flex: 1; + padding: 10px 16px; + border: none; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +button.primary { + background: #4fc3f7; + color: #000; +} + +button.primary:hover { + background: #29b6f6; +} + +button:not(.primary) { + background: #333; + color: #eee; +} + +button:not(.primary):hover { + background: #444; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} diff --git a/extension/popup/popup.html b/extension/popup/popup.html new file mode 100644 index 0000000..b638886 --- /dev/null +++ b/extension/popup/popup.html @@ -0,0 +1,55 @@ + + + + + + + +
+

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: idtitle for completion with description + for (const node of filtered.slice(0, 20)) { + console.log(`${node.id.slice(0, 8)}\t${node.title.slice(0, 40)}`); + } + } catch { + // Silently fail for completion scripts + } + }); + +export const getTagsCommand = new Command('--get-tags') + .description('Helper for shell completions') + .action(async () => { + try { + const db = getDb(); + const tags = db.prepare('SELECT DISTINCT tag FROM node_tags ORDER BY tag').all() as { tag: string }[]; + + for (const { tag } of tags.slice(0, 50)) { + console.log(tag); + } + } catch { + // Silently fail + } + }); + +export const getGraphsCommand = new Command('--get-graphs') + .description('Helper for shell completions') + .action(async () => { + try { + const graphs = listGraphs(); + for (const graph of graphs) { + console.log(graph.name); + } + } catch { + // Silently fail + } + }); diff --git a/src/cli/completions/index.ts b/src/cli/completions/index.ts new file mode 100644 index 0000000..65dcebf --- /dev/null +++ b/src/cli/completions/index.ts @@ -0,0 +1,397 @@ +/** + * Shell completion generators for Cortex CLI + */ + +export type Shell = 'bash' | 'zsh' | 'fish' | 'powershell'; + +const COMMANDS = [ + 'add', 'query', 'show', 'list', 'update', 'remove', 'link', 'graph', + 'children', 'decay', 'serve', 'history', 'diff', 'restore', 'capture', + 'context', 'config', 'index', 'journal', 'ingest', 'clip', 'export', + 'viz', 'import', 'backup', 'restore-backup', 'list-backups', 'graphs', + 'use', 'init', 'smart-search', 'ss', 'what', 'now', 'tui', 'ui', +]; + +const KINDS = ['memory', 'component', 'task', 'decision']; +const STATUSES = ['active', 'todo', 'in_progress', 'done', 'deprecated']; +const EDGE_TYPES = ['depends_on', 'contains', 'implements', 'blocked_by', 'subtask_of', 'relates_to', 'supersedes', 'about']; + +export function generateCompletions(shell: Shell): string { + switch (shell) { + case 'bash': + return generateBashCompletions(); + case 'zsh': + return generateZshCompletions(); + case 'fish': + return generateFishCompletions(); + case 'powershell': + return generatePowerShellCompletions(); + } +} + +function generateBashCompletions(): string { + return `# Cortex CLI completions for Bash +# Add to ~/.bashrc or /etc/bash_completion.d/cortex + +_cortex_completions() { + local cur prev words cword + _init_completion -n : || return + + local commands="${COMMANDS.join(' ')}" + local kinds="${KINDS.join(' ')}" + local statuses="${STATUSES.join(' ')}" + local edge_types="${EDGE_TYPES.join(' ')}" + + case "\${prev}" in + cortex) + COMPREPLY=($(compgen -W "\${commands}" -- "\${cur}")) + return 0 + ;; + add) + COMPREPLY=($(compgen -W "\${kinds}" -- "\${cur}")) + return 0 + ;; + --kind|-k) + COMPREPLY=($(compgen -W "\${kinds}" -- "\${cur}")) + return 0 + ;; + --status|-s) + COMPREPLY=($(compgen -W "\${statuses}" -- "\${cur}")) + return 0 + ;; + --type) + COMPREPLY=($(compgen -W "\${edge_types}" -- "\${cur}")) + return 0 + ;; + --tags|-t) + local tags=$(cortex --get-tags 2>/dev/null) + COMPREPLY=($(compgen -W "\${tags}" -- "\${cur}")) + return 0 + ;; + --graph) + local graphs=$(cortex --get-graphs 2>/dev/null) + COMPREPLY=($(compgen -W "\${graphs}" -- "\${cur}")) + return 0 + ;; + show|update|remove|children|history|diff|restore) + local nodes=$(cortex --get-nodes "\${cur}" 2>/dev/null) + COMPREPLY=($(compgen -W "\${nodes}" -- "\${cur}")) + return 0 + ;; + link) + local nodes=$(cortex --get-nodes "\${cur}" 2>/dev/null) + COMPREPLY=($(compgen -W "\${nodes}" -- "\${cur}")) + return 0 + ;; + use) + local graphs=$(cortex --get-graphs 2>/dev/null) + COMPREPLY=($(compgen -W "\${graphs}" -- "\${cur}")) + return 0 + ;; + import) + COMPREPLY=($(compgen -W "obsidian markdown" -- "\${cur}")) + return 0 + ;; + export|--format|-f) + COMPREPLY=($(compgen -W "html svg mermaid markdown jsonld" -- "\${cur}")) + return 0 + ;; + esac + + if [[ "\${cur}" == -* ]]; then + local opts="--help --version --kind --status --tags --limit --format --output --graph --all-graphs" + COMPREPLY=($(compgen -W "\${opts}" -- "\${cur}")) + return 0 + fi +} + +complete -F _cortex_completions cortex +`; +} + +function generateZshCompletions(): string { + return `#compdef cortex +# Cortex CLI completions for Zsh +# Add to ~/.zsh/completions/_cortex + +_cortex() { + local -a commands + commands=( + 'add:Add a node to the knowledge graph' + 'query:Search the knowledge graph' + 'show:Show a node and its connections' + 'list:List nodes' + 'update:Update a node' + 'remove:Remove a node' + 'link:Create a link between nodes' + 'graph:Display graph structure' + 'children:List child nodes' + 'decay:Mark old nodes as stale' + 'serve:Start web server' + 'history:View node version history' + 'diff:Compare node versions' + 'restore:Restore node to previous version' + 'capture:Capture conversation as memory' + 'context:Show context for session' + 'config:Configure capture settings' + 'index:Index project codebase' + 'journal:Daily journal' + 'ingest:Ingest content from URL' + 'clip:Quick clip a URL' + 'export:Export graph as HTML/SVG/Mermaid' + 'viz:Export interactive visualization' + 'import:Import from Obsidian/Markdown' + 'backup:Create database backup' + 'restore-backup:Restore from backup' + 'list-backups:List backup files' + 'graphs:Manage knowledge graphs' + 'use:Switch active graph' + 'init:Initialize project with graph' + 'smart-search:Context-aware search' + 'ss:Alias for smart-search' + 'what:What should I know right now?' + 'now:Show current context' + 'tui:Launch interactive TUI' + 'ui:Alias for tui' + ) + + local -a kinds=(memory component task decision) + local -a statuses=(active todo in_progress done deprecated) + local -a edge_types=(depends_on contains implements blocked_by subtask_of relates_to supersedes about) + + _arguments -C \\ + '1:command:->command' \\ + '*::arg:->args' + + case "$state" in + command) + _describe -t commands 'cortex commands' commands + ;; + args) + case "$words[1]" in + add) + _arguments \\ + '1:kind:(memory component task decision)' \\ + '--title[Node title]:title:' \\ + '--content[Node content]:content:' \\ + '--tags[Tags]:tags:->tags' \\ + '--status[Status]:status:(${STATUSES.join(' ')})' + ;; + show|update|remove|children|history) + _arguments '1:node:->nodes' + ;; + link) + _arguments \\ + '1:from:->nodes' \\ + '2:to:->nodes' \\ + '--type[Edge type]:type:(${EDGE_TYPES.join(' ')})' + ;; + list|query) + _arguments \\ + '--kind[Filter by kind]:kind:(${KINDS.join(' ')})' \\ + '--status[Filter by status]:status:(${STATUSES.join(' ')})' \\ + '--tags[Filter by tags]:tags:->tags' \\ + '--limit[Max results]:limit:' \\ + '--graph[Query specific graph]:graph:->graphs' \\ + '--all-graphs[Search all graphs]' + ;; + use) + _arguments '1:graph:->graphs' + ;; + import) + _arguments \\ + '1:source:(obsidian markdown)' \\ + '2:path:_files -/' + ;; + export) + _arguments \\ + '--format[Export format]:format:(html svg mermaid markdown jsonld)' \\ + '--output[Output file]:file:_files' \\ + '--kind[Filter by kind]:kind:(${KINDS.join(' ')})' + ;; + esac + ;; + esac + + case "$state" in + nodes) + local -a nodes + nodes=(\${(f)"$(cortex --get-nodes 2>/dev/null)"}) + _describe -t nodes 'nodes' nodes + ;; + tags) + local -a tags + tags=(\${(f)"$(cortex --get-tags 2>/dev/null)"}) + _describe -t tags 'tags' tags + ;; + graphs) + local -a graphs + graphs=(\${(f)"$(cortex --get-graphs 2>/dev/null)"}) + _describe -t graphs 'graphs' graphs + ;; + esac +} + +_cortex +`; +} + +function generateFishCompletions(): string { + return `# Cortex CLI completions for Fish +# Add to ~/.config/fish/completions/cortex.fish + +# Disable file completions for cortex +complete -c cortex -f + +# Main commands +complete -c cortex -n __fish_use_subcommand -a add -d 'Add a node' +complete -c cortex -n __fish_use_subcommand -a query -d 'Search the graph' +complete -c cortex -n __fish_use_subcommand -a show -d 'Show a node' +complete -c cortex -n __fish_use_subcommand -a list -d 'List nodes' +complete -c cortex -n __fish_use_subcommand -a update -d 'Update a node' +complete -c cortex -n __fish_use_subcommand -a remove -d 'Remove a node' +complete -c cortex -n __fish_use_subcommand -a link -d 'Link nodes' +complete -c cortex -n __fish_use_subcommand -a graph -d 'Display graph' +complete -c cortex -n __fish_use_subcommand -a children -d 'List children' +complete -c cortex -n __fish_use_subcommand -a decay -d 'Mark stale nodes' +complete -c cortex -n __fish_use_subcommand -a serve -d 'Start server' +complete -c cortex -n __fish_use_subcommand -a history -d 'View history' +complete -c cortex -n __fish_use_subcommand -a diff -d 'Compare versions' +complete -c cortex -n __fish_use_subcommand -a restore -d 'Restore version' +complete -c cortex -n __fish_use_subcommand -a capture -d 'Capture memory' +complete -c cortex -n __fish_use_subcommand -a context -d 'Show context' +complete -c cortex -n __fish_use_subcommand -a config -d 'Configure' +complete -c cortex -n __fish_use_subcommand -a index -d 'Index project' +complete -c cortex -n __fish_use_subcommand -a journal -d 'Daily journal' +complete -c cortex -n __fish_use_subcommand -a ingest -d 'Ingest URL' +complete -c cortex -n __fish_use_subcommand -a clip -d 'Clip URL' +complete -c cortex -n __fish_use_subcommand -a export -d 'Export graph' +complete -c cortex -n __fish_use_subcommand -a viz -d 'Visualize' +complete -c cortex -n __fish_use_subcommand -a import -d 'Import data' +complete -c cortex -n __fish_use_subcommand -a backup -d 'Backup database' +complete -c cortex -n __fish_use_subcommand -a restore-backup -d 'Restore backup' +complete -c cortex -n __fish_use_subcommand -a list-backups -d 'List backups' +complete -c cortex -n __fish_use_subcommand -a graphs -d 'Manage graphs' +complete -c cortex -n __fish_use_subcommand -a use -d 'Switch graph' +complete -c cortex -n __fish_use_subcommand -a init -d 'Init project' +complete -c cortex -n __fish_use_subcommand -a smart-search -d 'Smart search' +complete -c cortex -n __fish_use_subcommand -a ss -d 'Smart search alias' +complete -c cortex -n __fish_use_subcommand -a what -d 'What should I know?' +complete -c cortex -n __fish_use_subcommand -a now -d 'Current context' +complete -c cortex -n __fish_use_subcommand -a tui -d 'Interactive TUI' +complete -c cortex -n __fish_use_subcommand -a ui -d 'TUI alias' + +# Kind completions +complete -c cortex -n '__fish_seen_subcommand_from add' -a 'memory component task decision' + +# Node ID completions +complete -c cortex -n '__fish_seen_subcommand_from show update remove children history' -a '(cortex --get-nodes 2>/dev/null)' + +# Graph completions +complete -c cortex -n '__fish_seen_subcommand_from use' -a '(cortex --get-graphs 2>/dev/null)' + +# Import source completions +complete -c cortex -n '__fish_seen_subcommand_from import' -a 'obsidian markdown' + +# Export format completions +complete -c cortex -n '__fish_seen_subcommand_from export' -l format -a 'html svg mermaid markdown jsonld' + +# Common options +complete -c cortex -l kind -s k -d 'Filter by kind' -a 'memory component task decision' +complete -c cortex -l status -s s -d 'Filter by status' -a 'active todo in_progress done deprecated' +complete -c cortex -l tags -s t -d 'Filter by tags' -a '(cortex --get-tags 2>/dev/null)' +complete -c cortex -l limit -s l -d 'Max results' +complete -c cortex -l graph -d 'Specific graph' -a '(cortex --get-graphs 2>/dev/null)' +complete -c cortex -l all-graphs -d 'Search all graphs' +complete -c cortex -l help -s h -d 'Show help' +`; +} + +function generatePowerShellCompletions(): string { + return `# Cortex CLI completions for PowerShell +# Add to your $PROFILE + +Register-ArgumentCompleter -Native -CommandName cortex -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) + + $commands = @('add', 'query', 'show', 'list', 'update', 'remove', 'link', 'graph', + 'children', 'decay', 'serve', 'history', 'diff', 'restore', 'capture', + 'context', 'config', 'index', 'journal', 'ingest', 'clip', 'export', + 'viz', 'import', 'backup', 'restore-backup', 'list-backups', 'graphs', + 'use', 'init', 'smart-search', 'ss', 'what', 'now', 'tui', 'ui') + $kinds = @('memory', 'component', 'task', 'decision') + $statuses = @('active', 'todo', 'in_progress', 'done', 'deprecated') + $edgeTypes = @('depends_on', 'contains', 'implements', 'blocked_by', 'subtask_of', 'relates_to', 'supersedes', 'about') + + $elements = $commandAst.CommandElements + $command = if ($elements.Count -gt 1) { $elements[1].Value } else { $null } + + # Command completion + if ($elements.Count -le 2) { + $commands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + return + } + + # Sub-command argument completion + switch ($command) { + 'add' { + if ($elements.Count -eq 3) { + $kinds | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + } + { $_ -in @('show', 'update', 'remove', 'children', 'history') } { + $nodes = cortex --get-nodes $wordToComplete 2>$null + if ($nodes) { + $nodes -split "\\n" | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + } + 'use' { + $graphs = cortex --get-graphs 2>$null + if ($graphs) { + $graphs -split "\\n" | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + } + 'import' { + if ($elements.Count -eq 3) { + @('obsidian', 'markdown') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + } + } + + # Option completion + $prevElement = if ($elements.Count -gt 2) { $elements[-2].Value } else { $null } + switch ($prevElement) { + { $_ -in @('--kind', '-k') } { + $kinds | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + { $_ -in @('--status', '-s') } { + $statuses | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + '--type' { + $edgeTypes | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + { $_ -in @('--format', '-f') } { + @('html', 'svg', 'mermaid', 'markdown', 'jsonld') | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object { + [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) + } + } + } +} +`; +} diff --git a/src/cli/index.ts b/src/cli/index.ts index adf3ba7..7df04fd 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -25,6 +25,7 @@ import { backupCommand, restoreDbCommand, listBackupsCommand } from './commands/ import { graphsCommand, useCommand, initCommand } from './commands/graphs'; import { smartSearchCommand, ssCommand, whatCommand, contextAwareCommand } from './commands/smart'; import { tuiCommand, uiCommand } from './commands/tui'; +import { completionsCommand, getNodesCommand, getTagsCommand, getGraphsCommand } from './commands/completions'; import { closeDb } from '../core/db'; import { migrateOldDatabase } from '../core/db'; @@ -75,6 +76,10 @@ program.addCommand(whatCommand); program.addCommand(contextAwareCommand); program.addCommand(tuiCommand); program.addCommand(uiCommand); +program.addCommand(completionsCommand); +program.addCommand(getNodesCommand); +program.addCommand(getTagsCommand); +program.addCommand(getGraphsCommand); // Check for old database migration migrateOldDatabase();