Add graph visualization exports (Milestone 7)
- Add interactive HTML export with D3.js force-directed graph - Add SVG export with simple force-directed layout - Add Mermaid diagram export for documentation - Support subgraph export from root node with depth - Add node filtering by kind and tags - Add light/dark theme support - Add CLI commands: export, viz - Add MCP tool: memory_export
This commit is contained in:
84
src/cli/commands/export.ts
Normal file
84
src/cli/commands/export.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Command } from 'commander';
|
||||
import * as fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import { exportGraph, ExportFormat } from '../../core/export';
|
||||
|
||||
export const exportCommand = new Command('export')
|
||||
.description('Export the knowledge graph as HTML, SVG, or Mermaid')
|
||||
.argument('[rootId]', 'Root node ID for subgraph export')
|
||||
.option('-f, --format <format>', 'Output format: html, svg, mermaid', 'html')
|
||||
.option('-o, --output <file>', 'Output file path')
|
||||
.option('-d, --depth <n>', 'Depth for subgraph export', '3')
|
||||
.option('-k, --kind <kind>', 'Filter by node kind')
|
||||
.option('-t, --tags <tags>', 'Filter by tags (comma-separated)')
|
||||
.option('--theme <theme>', 'Theme: light or dark', 'dark')
|
||||
.option('--width <n>', 'Width for SVG', '800')
|
||||
.option('--height <n>', 'Height for SVG', '600')
|
||||
.option('--direction <dir>', 'Mermaid direction: TD, LR, BT, RL', 'TD')
|
||||
.option('--title <title>', 'Title for HTML export')
|
||||
.action(async (rootId: string | undefined, opts) => {
|
||||
try {
|
||||
const format = opts.format.toLowerCase() as ExportFormat;
|
||||
if (!['html', 'svg', 'mermaid'].includes(format)) {
|
||||
console.error(chalk.red(`Invalid format: ${format}. Use html, svg, or mermaid.`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`Exporting graph as ${format}...`));
|
||||
|
||||
const content = await exportGraph({
|
||||
format,
|
||||
rootId,
|
||||
depth: parseInt(opts.depth),
|
||||
kind: opts.kind,
|
||||
tags: opts.tags?.split(',').map((t: string) => t.trim()),
|
||||
theme: opts.theme,
|
||||
width: parseInt(opts.width),
|
||||
height: parseInt(opts.height),
|
||||
direction: opts.direction,
|
||||
title: opts.title,
|
||||
});
|
||||
|
||||
if (opts.output) {
|
||||
fs.writeFileSync(opts.output, content);
|
||||
console.log(chalk.green(`✓ Exported to ${opts.output}`));
|
||||
|
||||
// Show file size
|
||||
const stats = fs.statSync(opts.output);
|
||||
console.log(chalk.dim(` Size: ${(stats.size / 1024).toFixed(1)} KB`));
|
||||
} else {
|
||||
// Output to stdout
|
||||
console.log(content);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Shorthand for HTML visualization
|
||||
export const vizCommand = new Command('viz')
|
||||
.description('Export interactive HTML visualization')
|
||||
.argument('[rootId]', 'Root node ID for subgraph')
|
||||
.option('-o, --output <file>', 'Output file', 'graph.html')
|
||||
.option('--theme <theme>', 'Theme: light or dark', 'dark')
|
||||
.option('-d, --depth <n>', 'Depth for subgraph', '3')
|
||||
.action(async (rootId: string | undefined, opts) => {
|
||||
try {
|
||||
console.log(chalk.cyan('Generating visualization...'));
|
||||
|
||||
const content = await exportGraph({
|
||||
format: 'html',
|
||||
rootId,
|
||||
depth: parseInt(opts.depth),
|
||||
theme: opts.theme,
|
||||
});
|
||||
|
||||
fs.writeFileSync(opts.output, content);
|
||||
console.log(chalk.green(`✓ Created ${opts.output}`));
|
||||
console.log(chalk.dim(` Open in browser to view interactive graph`));
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -19,6 +19,7 @@ import { contextCommand, contextHookCommand } from './commands/context';
|
||||
import { indexCommand } from './commands/index-cmd';
|
||||
import { journalCommand, journalAliasCommand, quickCaptureCommand } from './commands/journal';
|
||||
import { ingestCommand, clipCommand } from './commands/ingest';
|
||||
import { exportCommand, vizCommand } from './commands/export';
|
||||
import { closeDb } from '../core/db';
|
||||
|
||||
const program = new Command();
|
||||
@@ -53,6 +54,8 @@ program.addCommand(journalAliasCommand);
|
||||
program.addCommand(quickCaptureCommand);
|
||||
program.addCommand(ingestCommand);
|
||||
program.addCommand(clipCommand);
|
||||
program.addCommand(exportCommand);
|
||||
program.addCommand(vizCommand);
|
||||
|
||||
program.hook('postAction', () => {
|
||||
closeDb();
|
||||
|
||||
Reference in New Issue
Block a user