#!/usr/bin/env nu # Export KOGRAL to Logseq format # # Usage: nu kogral-export-logseq.nu [--kogral-dir ] [--dry-run] def main [ output_path: string # Path for Logseq graph output --kogral-dir: string = ".kogral" # KOGRAL directory --dry-run # Show what would be exported without making changes --skip-journals # Skip exporting journal entries ] { print $"(ansi green_bold)Logseq Export(ansi reset)" print $"Source: ($kogral_dir)" print $"Target: ($output_path)" if $dry_run { print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)" } # Check if .kogral directory exists if not ($kogral_dir | path exists) { print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)" exit 1 } # Count files to export let stats = get_export_stats $kogral_dir $skip_journals print $"\n(ansi cyan_bold)Files to export:(ansi reset)" print $" Notes: ($stats.notes)" print $" Decisions: ($stats.decisions)" print $" Guidelines: ($stats.guidelines)" print $" Patterns: ($stats.patterns)" print $" Journals: ($stats.journals)" print $" Total: ($stats.total)" if $stats.total == 0 { print $"\n(ansi yellow)No files to export(ansi reset)" exit 0 } if $dry_run { print $"\n(ansi yellow)[DRY RUN] Would export ($stats.total) files(ansi reset)" exit 0 } # Create Logseq directory structure print $"\n(ansi cyan_bold)Creating Logseq directory structure...(ansi reset)" create_logseq_structure $output_path # Export files print $"\n(ansi cyan_bold)Exporting files...(ansi reset)" export_nodes $"($kogral_dir)/notes" $"($output_path)/pages" "note" export_nodes $"($kogral_dir)/decisions" $"($output_path)/pages" "decision" export_nodes $"($kogral_dir)/guidelines" $"($output_path)/pages" "guideline" export_nodes $"($kogral_dir)/patterns" $"($output_path)/pages" "pattern" if not $skip_journals { export_journals $"($kogral_dir)/journal" $"($output_path)/journals" } # Create Logseq config print $"\n(ansi cyan_bold)Creating Logseq configuration...(ansi reset)" create_logseq_config $output_path print $"\n(ansi green_bold)✓ Export completed(ansi reset)" print $"Exported ($stats.total) files to ($output_path)" } def get_export_stats [kogral_dir: string, skip_journals: bool] { let notes = if ($"($kogral_dir)/notes" | path exists) { glob $"($kogral_dir)/notes/**/*.md" | length } else { 0 } let decisions = if ($"($kogral_dir)/decisions" | path exists) { glob $"($kogral_dir)/decisions/**/*.md" | length } else { 0 } let guidelines = if ($"($kogral_dir)/guidelines" | path exists) { glob $"($kogral_dir)/guidelines/**/*.md" | length } else { 0 } let patterns = if ($"($kogral_dir)/patterns" | path exists) { glob $"($kogral_dir)/patterns/**/*.md" | length } else { 0 } let journals = if not $skip_journals and ($"($kogral_dir)/journal" | path exists) { glob $"($kogral_dir)/journal/**/*.md" | length } else { 0 } { notes: $notes, decisions: $decisions, guidelines: $guidelines, patterns: $patterns, journals: $journals, total: ($notes + $decisions + $guidelines + $patterns + $journals) } } def create_logseq_structure [output_path: string] { mkdir $output_path mkdir $"($output_path)/pages" mkdir $"($output_path)/journals" mkdir $"($output_path)/assets" mkdir $"($output_path)/logseq" print $" (ansi green)✓ Directory structure created(ansi reset)" } def export_nodes [source_dir: string, target_dir: string, node_type: string] { if not ($source_dir | path exists) { return } let files = glob $"($source_dir)/**/*.md" if ($files | length) == 0 { return } print $"\n Exporting ($node_type)s..." mut exported = 0 let total = $files | length for file in $files { let filename = $file | path basename # Phase 1: Read KOGRAL markdown file let content = open $file # Phase 2: Convert to Logseq format let logseq_content = convert_kogral_to_logseq $content $node_type # Phase 3: Save to Logseq pages directory $logseq_content | save $"($target_dir)/($filename)" $exported = $exported + 1 } print $" (ansi green)✓ Exported ($exported)/($total) ($node_type)s(ansi reset)" } def export_journals [source_dir: string, target_dir: string] { if not ($source_dir | path exists) { return } let files = glob $"($source_dir)/**/*.md" if ($files | length) == 0 { return } print $"\n Exporting journals..." mut exported = 0 let total = $files | length for file in $files { let filename = $file | path basename # Phase 1: Read KOGRAL journal file let content = open $file # Phase 2: Convert to Logseq format let logseq_content = convert_kogral_to_logseq $content "journal" # Phase 3: Save to Logseq journals directory $logseq_content | save $"($target_dir)/($filename)" $exported = $exported + 1 } print $" (ansi green)✓ Exported ($exported)/($total) journals(ansi reset)" } def convert_kogral_to_logseq [content: string, node_type: string] { let lines = $content | lines # Phase 1: Check for and parse YAML frontmatter let has_frontmatter = ($lines | get 0 | str trim) == "---" if not $has_frontmatter { # No frontmatter, return content as-is with minimal properties return $"type:: ($node_type)\n\n$content" } # Phase 2: Extract frontmatter and body let frontmatter_end = get_frontmatter_end_index $lines let frontmatter_lines = $lines | take $frontmatter_end let body_lines = $lines | skip $frontmatter_end # Phase 3: Parse YAML fields let fm = parse_yaml_frontmatter $frontmatter_lines # Phase 4: Convert to Logseq properties format mut logseq_props = "" # Add type property $logseq_props = $logseq_props + $"type:: ($node_type)\n" # Add title if present if not ($fm.title? | is-empty) { $logseq_props = $logseq_props + $"title:: ($fm.title?)\n" } # Add created date if present if not ($fm.created? | is-empty) { let created_date = convert_date_to_logseq $fm.created? $logseq_props = $logseq_props + $"created:: [[$created_date]]\n" } # Add tags if present if not ($fm.tags? | is-empty) { $logseq_props = $logseq_props + "tags:: " let tags_list = $fm.tags? | str replace '\[' '' | str replace '\]' '' | split row ',' for tag in $tags_list { let trimmed = $tag | str trim | str replace '"' '' $logseq_props = $logseq_props + $"[[($trimmed)]] " } $logseq_props = $logseq_props + "\n" } # Add status if present if not ($fm.status? | is-empty) { $logseq_props = $logseq_props + $"status:: ($fm.status?)\n" } # Add relationships if present if not ($fm.relates_to? | is-empty) { $logseq_props = $logseq_props + "relates-to:: " let refs = parse_yaml_list $fm.relates_to? let refs_formatted = $refs | each { |r| $'[[($r)]]' } $logseq_props = $logseq_props + ($refs_formatted | str join ", ") + "\n" } if not ($fm.depends_on? | is-empty) { $logseq_props = $logseq_props + "depends-on:: " let refs = parse_yaml_list $fm.depends_on? let refs_formatted = $refs | each { |r| $'[[($r)]]' } $logseq_props = $logseq_props + ($refs_formatted | str join ", ") + "\n" } # Phase 5: Build final output let body = $body_lines | str join "\n" $"$logseq_props\n$body" } def get_frontmatter_end_index [lines: list] { mut idx = 1 # Skip first "---" for line in ($lines | skip 1) { if ($line | str trim) == "---" { return ($idx + 1) } $idx = $idx + 1 } $idx } def parse_yaml_frontmatter [lines: list] { mut fm = {} for line in $lines { if ($line | str trim) == "---" { continue } # Match YAML key: value format if ($line =~ '^[\w]+:') { let key = $line | str replace '^(\w+):.*' '$1' let value = $line | str replace '^[\w]+:\s*' '' | str trim $fm = ($fm | insert $key $value) } } $fm } def convert_date_to_logseq [date_str: string] { # Convert ISO 8601 (2026-01-17T10:30:00Z) to Logseq format (Jan 17th, 2026) # For simplicity, extract date part and format let date_part = $date_str | str substring 0..10 let year = $date_part | str substring 0..4 let month = $date_part | str substring 5..7 let day = $date_part | str substring 8..10 | str replace '^0+' '' let month_name = match $month { "01" => "Jan", "02" => "Feb", "03" => "Mar", "04" => "Apr", "05" => "May", "06" => "Jun", "07" => "Jul", "08" => "Aug", "09" => "Sep", "10" => "Oct", "11" => "Nov", "12" => "Dec", _ => "Unknown" } let day_suffix = match ($day | into int) { 1 | 21 | 31 => "st", 2 | 22 => "nd", 3 | 23 => "rd", _ => "th" } $"($month_name) ($day)($day_suffix), ($year)" } def parse_yaml_list [yaml_str: string] { # Parse YAML list format: [item1, item2] or list format with dashes # For now, handle bracket format let cleaned = $yaml_str | str replace '\[' '' | str replace '\]' '' let items = $cleaned | split row ',' | map { |i| $i | str trim | str replace '"' '' } $items } def create_logseq_config [output_path: string] { let config = { "preferred-format": "markdown", "preferred-workflow": ":now", "hidden": [".git"], "journal/page-title-format": "yyyy-MM-dd", "start-of-week": 1, "feature/enable-block-timestamps": false, "feature/enable-search-remove-accents": true } $config | to json | save $"($output_path)/logseq/config.edn" print $" (ansi green)✓ Logseq configuration created(ansi reset)" }