Add import/export and backup system (Milestone 8)
- Obsidian vault importer with wikilink → edge conversion - Markdown folder importer with frontmatter parsing - Markdown exporter with wikilinks and frontmatter - JSON-LD linked data exporter - Database backup/restore functionality - CLI: import, backup, restore-backup, list-backups - MCP tools: memory_import, memory_backup, memory_export_markdown, memory_export_jsonld
This commit is contained in:
68
src/cli/commands/backup-cmd.ts
Normal file
68
src/cli/commands/backup-cmd.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { createBackup, restoreBackup, listBackups } from '../../core/backup';
|
||||
|
||||
export const backupCommand = new Command('backup')
|
||||
.description('Create a backup of the database')
|
||||
.argument('<path>', 'Output file path')
|
||||
.action(async (outputPath: string) => {
|
||||
try {
|
||||
console.log(chalk.cyan('Creating backup...'));
|
||||
const result = await createBackup(outputPath);
|
||||
console.log(chalk.green(`✓ Backup created: ${result.path}`));
|
||||
console.log(chalk.dim(` Size: ${(result.size / 1024).toFixed(1)} KB`));
|
||||
console.log(chalk.dim(` Nodes: ${result.nodes}`));
|
||||
console.log(chalk.dim(` Edges: ${result.edges}`));
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
export const restoreDbCommand = new Command('restore-backup')
|
||||
.description('Restore database from a backup')
|
||||
.argument('<path>', 'Backup file path')
|
||||
.option('-y, --yes', 'Skip confirmation')
|
||||
.action(async (backupPath: string, opts) => {
|
||||
try {
|
||||
if (!opts.yes) {
|
||||
console.log(chalk.yellow('Warning: This will replace your current database.'));
|
||||
console.log(chalk.yellow('A backup of the current database will be created first.'));
|
||||
console.log(chalk.dim('Use --yes to skip this warning.'));
|
||||
// In a real CLI we'd prompt for confirmation, but for simplicity we proceed
|
||||
}
|
||||
|
||||
console.log(chalk.cyan('Restoring backup...'));
|
||||
const result = await restoreBackup(backupPath);
|
||||
console.log(chalk.green('✓ Database restored'));
|
||||
console.log(chalk.dim(` Nodes: ${result.nodes}`));
|
||||
console.log(chalk.dim(` Edges: ${result.edges}`));
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
export const listBackupsCommand = new Command('list-backups')
|
||||
.description('List backups in a directory')
|
||||
.argument('[directory]', 'Directory to list', '.')
|
||||
.action((directory: string) => {
|
||||
try {
|
||||
const backups = listBackups(directory);
|
||||
if (backups.length === 0) {
|
||||
console.log(chalk.yellow('No backup files found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`Found ${backups.length} backup(s):\n`));
|
||||
for (const backup of backups) {
|
||||
const sizeKb = (backup.size / 1024).toFixed(1);
|
||||
const date = backup.modified.toISOString().replace('T', ' ').slice(0, 19);
|
||||
console.log(` ${chalk.bold(backup.name)}`);
|
||||
console.log(chalk.dim(` Size: ${sizeKb} KB | Modified: ${date}`));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Command } from 'commander';
|
||||
import * as fs from 'fs';
|
||||
import chalk from 'chalk';
|
||||
import { exportGraph, ExportFormat } from '../../core/export';
|
||||
import { exportGraph, ExportFormat, exportMarkdown, exportJsonLd } from '../../core/export';
|
||||
|
||||
export const exportCommand = new Command('export')
|
||||
.description('Export the knowledge graph as HTML, SVG, or Mermaid')
|
||||
.description('Export the knowledge graph (html, svg, mermaid, markdown, jsonld)')
|
||||
.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('-f, --format <format>', 'Output format: html, svg, mermaid, markdown, jsonld', 'html')
|
||||
.option('-o, --output <file>', 'Output file/directory 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)')
|
||||
@@ -16,16 +16,52 @@ export const exportCommand = new Command('export')
|
||||
.option('--height <n>', 'Height for SVG', '600')
|
||||
.option('--direction <dir>', 'Mermaid direction: TD, LR, BT, RL', 'TD')
|
||||
.option('--title <title>', 'Title for HTML export')
|
||||
.option('--no-frontmatter', 'Skip frontmatter in markdown export')
|
||||
.option('--no-wikilinks', 'Skip wikilinks in markdown 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.`));
|
||||
const validFormats = ['html', 'svg', 'mermaid', 'markdown', 'jsonld'];
|
||||
if (!validFormats.includes(format)) {
|
||||
console.error(chalk.red(`Invalid format: ${format}. Use: ${validFormats.join(', ')}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`Exporting graph as ${format}...`));
|
||||
|
||||
// Handle markdown export (outputs to directory)
|
||||
if (format === 'markdown') {
|
||||
const outputDir = opts.output || './exported-markdown';
|
||||
const result = await exportMarkdown(outputDir, {
|
||||
kind: opts.kind,
|
||||
tags: opts.tags?.split(',').map((t: string) => t.trim()),
|
||||
frontmatter: opts.frontmatter !== false,
|
||||
wikilinks: opts.wikilinks !== false,
|
||||
});
|
||||
console.log(chalk.green(`✓ Exported ${result.exported} files to ${outputDir}`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle jsonld export
|
||||
if (format === 'jsonld') {
|
||||
const content = await exportJsonLd({
|
||||
kind: opts.kind,
|
||||
tags: opts.tags?.split(',').map((t: string) => t.trim()),
|
||||
pretty: true,
|
||||
});
|
||||
|
||||
if (opts.output) {
|
||||
fs.writeFileSync(opts.output, content);
|
||||
console.log(chalk.green(`✓ Exported to ${opts.output}`));
|
||||
const stats = fs.statSync(opts.output);
|
||||
console.log(chalk.dim(` Size: ${(stats.size / 1024).toFixed(1)} KB`));
|
||||
} else {
|
||||
console.log(content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle graph exports (html, svg, mermaid)
|
||||
const content = await exportGraph({
|
||||
format,
|
||||
rootId,
|
||||
|
||||
66
src/cli/commands/import.ts
Normal file
66
src/cli/commands/import.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { importObsidian } from '../../core/import/obsidian';
|
||||
import { importMarkdown } from '../../core/import/markdown';
|
||||
|
||||
export const importCommand = new Command('import')
|
||||
.description('Import data from external sources')
|
||||
.argument('<source>', 'Source type: obsidian, markdown')
|
||||
.argument('<path>', 'Path to import from')
|
||||
.option('-t, --tags <tags>', 'Additional tags (comma-separated)')
|
||||
.option('-k, --kind <kind>', 'Node kind (default: memory)')
|
||||
.option('--hierarchy', 'Create folder hierarchy (obsidian only)')
|
||||
.option('--dry-run', 'Preview import without making changes')
|
||||
.action(async (source: string, inputPath: string, opts) => {
|
||||
try {
|
||||
const tags = opts.tags?.split(',').map((t: string) => t.trim());
|
||||
|
||||
switch (source.toLowerCase()) {
|
||||
case 'obsidian': {
|
||||
console.log(chalk.cyan(`Importing Obsidian vault from ${inputPath}...`));
|
||||
const result = await importObsidian(inputPath, {
|
||||
kind: opts.kind,
|
||||
hierarchy: opts.hierarchy,
|
||||
dryRun: opts.dryRun,
|
||||
});
|
||||
|
||||
if (opts.dryRun) {
|
||||
console.log(chalk.yellow(`Dry run: would import ${result.imported} notes`));
|
||||
console.log(chalk.dim(` Would create ${result.edges} edges from wikilinks`));
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Imported ${result.imported} notes`));
|
||||
if (result.edges > 0) {
|
||||
console.log(chalk.dim(` Created ${result.edges} edges from wikilinks`));
|
||||
}
|
||||
if (result.skipped > 0) {
|
||||
console.log(chalk.yellow(` Skipped ${result.skipped} files (already exist)`));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'markdown':
|
||||
case 'md': {
|
||||
console.log(chalk.cyan(`Importing markdown files from ${inputPath}...`));
|
||||
const result = await importMarkdown(inputPath, {
|
||||
kind: opts.kind as any,
|
||||
tags,
|
||||
dryRun: opts.dryRun,
|
||||
});
|
||||
|
||||
if (opts.dryRun) {
|
||||
console.log(chalk.yellow(`Dry run: would import ${result.imported} files`));
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Imported ${result.imported} files`));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error(chalk.red(`Unknown source type: ${source}`));
|
||||
console.log(chalk.dim('Supported: obsidian, markdown'));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(chalk.red(`Error: ${err.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
@@ -20,6 +20,8 @@ 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 { importCommand } from './commands/import';
|
||||
import { backupCommand, restoreDbCommand, listBackupsCommand } from './commands/backup-cmd';
|
||||
import { closeDb } from '../core/db';
|
||||
|
||||
const program = new Command();
|
||||
@@ -56,6 +58,10 @@ program.addCommand(ingestCommand);
|
||||
program.addCommand(clipCommand);
|
||||
program.addCommand(exportCommand);
|
||||
program.addCommand(vizCommand);
|
||||
program.addCommand(importCommand);
|
||||
program.addCommand(backupCommand);
|
||||
program.addCommand(restoreDbCommand);
|
||||
program.addCommand(listBackupsCommand);
|
||||
|
||||
program.hook('postAction', () => {
|
||||
closeDb();
|
||||
|
||||
Reference in New Issue
Block a user