#!/usr/bin/env nu # Import from Logseq graph to KOGRAL # # Usage: nu kogral-import-logseq.nu [--kogral-dir ] [--dry-run] def main [ logseq_path: string # Path to Logseq graph directory --kogral-dir: string = ".kogral" # KOGRAL directory --dry-run # Show what would be imported without making changes --skip-journals # Skip importing journal entries --skip-pages # Skip importing pages ] { print $"(ansi green_bold)Logseq Import(ansi reset)" print $"Source: ($logseq_path)" print $"Target: ($kogral_dir)" if $dry_run { print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)" } # Validate Logseq directory if not ($logseq_path | path exists) { print $"(ansi red)Error: Logseq path not found: ($logseq_path)(ansi reset)" exit 1 } let pages_dir = $"($logseq_path)/pages" let journals_dir = $"($logseq_path)/journals" if not ($pages_dir | path exists) and not ($journals_dir | path exists) { print $"(ansi red)Error: Not a valid Logseq graph (missing pages or journals directory)(ansi reset)" exit 1 } # Validate KOGRAL directory if not ($kogral_dir | path exists) { print $"(ansi yellow)KOGRAL directory doesn't exist. Creating...(ansi reset)" if not $dry_run { mkdir $kogral_dir mkdir $"($kogral_dir)/notes" mkdir $"($kogral_dir)/journal" } } # Count files to import let pages_count = if ($pages_dir | path exists) and not $skip_pages { glob $"($pages_dir)/**/*.md" | length } else { 0 } let journals_count = if ($journals_dir | path exists) and not $skip_journals { glob $"($journals_dir)/**/*.md" | length } else { 0 } print $"\n(ansi cyan_bold)Files to import:(ansi reset)" print $" Pages: ($pages_count)" print $" Journals: ($journals_count)" print $" Total: ($pages_count + $journals_count)" if ($pages_count + $journals_count) == 0 { print $"\n(ansi yellow)No files to import(ansi reset)" exit 0 } if $dry_run { print $"\n(ansi yellow)[DRY RUN] Would import ($pages_count + $journals_count) files(ansi reset)" exit 0 } # Import pages if not $skip_pages and $pages_count > 0 { print $"\n(ansi cyan_bold)Importing pages...(ansi reset)" import_pages $pages_dir $kogral_dir } # Import journals if not $skip_journals and $journals_count > 0 { print $"\n(ansi cyan_bold)Importing journals...(ansi reset)" import_journals $journals_dir $kogral_dir } print $"\n(ansi green_bold)✓ Import completed(ansi reset)" print $"Imported ($pages_count + $journals_count) files" } def import_pages [pages_dir: string, kogral_dir: string] { let files = glob $"($pages_dir)/**/*.md" let total = $files | length mut imported = 0 mut decisions = 0 mut guidelines = 0 mut patterns = 0 mut notes = 0 for file in $files { let filename = $file | path basename print $" Importing ($filename)..." # Phase 1: Read and parse Logseq format let content = open $file # Phase 2: Detect node type from properties/content let node_type = detect_node_type $content # Phase 3: Convert to KOGRAL format let kogral_content = convert_logseq_to_kogral $content $node_type # Phase 4: Determine target directory let target_dir = match $node_type { "decision" => { $decisions = $decisions + 1 $"($kogral_dir)/decisions" }, "guideline" => { $guidelines = $guidelines + 1 $"($kogral_dir)/guidelines" }, "pattern" => { $patterns = $patterns + 1 $"($kogral_dir)/patterns" }, _ => { $notes = $notes + 1 $"($kogral_dir)/notes" } } # Phase 5: Save to KOGRAL mkdir $target_dir $kogral_content | save $"($target_dir)/($filename)" $imported = $imported + 1 print $" (ansi green)✓ Imported as ($node_type)(ansi reset)" } print $"\n(ansi green)Pages summary:(ansi reset)" print $" Notes: ($notes)" print $" Decisions: ($decisions)" print $" Guidelines: ($guidelines)" print $" Patterns: ($patterns)" print $" Total: ($imported)/($total) imported" } def import_journals [journals_dir: string, kogral_dir: string] { let files = glob $"($journals_dir)/**/*.md" let total = $files | length mkdir $"($kogral_dir)/journal" mut imported = 0 for file in $files { let filename = $file | path basename print $" Importing ($filename)..." # Phase 1: Read Logseq journal format let content = open $file # Phase 2: Convert to KOGRAL journal format let kogral_content = convert_logseq_to_kogral $content "journal" # Phase 3: Save to journal directory $kogral_content | save $"($kogral_dir)/journal/($filename)" $imported = $imported + 1 print $" (ansi green)✓ Imported(ansi reset)" } print $"\n(ansi green)Journals imported: ($imported)/($total)(ansi reset)" } def detect_node_type [content: string] { # Check for type hints in properties or content if ($content | str contains "type:: decision") or ($content | str contains "# Decision") { "decision" } else if ($content | str contains "type:: guideline") or ($content | str contains "# Guideline") { "guideline" } else if ($content | str contains "type:: pattern") or ($content | str contains "# Pattern") { "pattern" } else { "note" } } def convert_logseq_to_kogral [content: string, node_type: string] { let lines = $content | lines # Phase 1: Parse Logseq properties (key:: value format) let props = parse_logseq_properties $lines let body_start = get_body_start_index $lines # Phase 2: Extract metadata from properties let title = $props.title? | default (extract_title_from_lines $lines) let created = $props.created? | default (date now | format date "%Y-%m-%dT%H:%M:%SZ") let modified = $props.modified? | default (date now | format date "%Y-%m-%dT%H:%M:%SZ") let status = match ($node_type) { "journal" => "draft", _ => "active" } # Phase 3: Extract tags and relationships from properties let tags = parse_tags_from_properties $props let relates_to = parse_references_from_property ($props.relates_to? | default "") let depends_on = parse_references_from_property ($props.depends_on? | default "") let implements = parse_references_from_property ($props.implements? | default "") let extends = parse_references_from_property ($props.extends? | default "") # Phase 4: Build YAML frontmatter let frontmatter = build_yaml_frontmatter { type: $node_type, title: $title, created: $created, modified: $modified, status: $status, tags: $tags, relates_to: $relates_to, depends_on: $depends_on, implements: $implements, extends: $extends } # Phase 5: Extract body and preserve wikilinks let body = if $body_start < ($lines | length) { $lines | skip $body_start | str join "\n" } else { "" } # Phase 6: Convert Logseq-specific syntax let converted_body = convert_logseq_syntax $body $"$frontmatter\n$converted_body" } def parse_logseq_properties [lines: list] { mut props = {} # Parse properties until blank line or content starts for line in $lines { if ($line | str trim | is-empty) { break } # Match pattern: key:: value if ($line =~ '^[\w-]+::') { let key = $line | str replace '^(\w[\w-]*)::.*' '$1' let value = $line | str replace '^[\w-]+::\s*' '' | str trim $props = ($props | insert $key $value) } } $props } def get_body_start_index [lines: list] { # Find where properties end (first blank line or non-property line) mut idx = 0 for line in $lines { if ($line | str trim | is-empty) { return ($idx + 1) } if not ($line =~ '^[\w-]+::') and ($line | str trim | length) > 0 { return $idx } $idx = $idx + 1 } $idx } def extract_title_from_lines [lines: list] { # Extract from first heading or property for line in $lines { if ($line =~ '^#+ ') { return ($line | str replace '^#+\s+' '') } if ($line =~ '^title::') { return ($line | str replace '^title::\s*' '') } } "Untitled" } def parse_tags_from_properties [props: record] { mut tags = [] # Check tags property if ($props.tags? | is-empty) { return $tags } let tags_str = $props.tags? # Extract [[tag]] format using split if ($tags_str | str contains "[[") { let parts = $tags_str | split row "[[" | skip 1 for part in $parts { let tag = $part | split row "]]" | get 0 if ($tag | str length) > 0 { $tags = ($tags | append $tag) } } } else { # Extract comma-separated format $tags = ($tags_str | split row ',' | each { |t| $t | str trim }) } $tags } def parse_references_from_property [prop: string] { if ($prop | str length) == 0 { return [] } # Extract [[ref]] format using split mut refs = [] if ($prop | str contains "[[") { let parts = $prop | split row "[[" | skip 1 for part in $parts { let ref = $part | split row "]]" | get 0 if ($ref | str length) > 0 { $refs = ($refs | append $ref) } } } $refs } def build_yaml_frontmatter [data: record] { mut fm = "---\n" $fm = $fm + $"type: ($data.type)\n" $fm = $fm + $"title: ($data.title)\n" $fm = $fm + $"created: ($data.created)\n" $fm = $fm + $"modified: ($data.modified)\n" $fm = $fm + $"status: ($data.status)\n" if ($data.tags | length) > 0 { let quoted_tags = $data.tags | each { |t| $'"($t)"' } let tags_str = $quoted_tags | str join ", " $fm = $fm + $"tags: [$tags_str]\n" } if ($data.relates_to | length) > 0 { $fm = $fm + "relates_to:\n" for ref in $data.relates_to { $fm = $fm + $" - $ref\n" } } if ($data.depends_on | length) > 0 { $fm = $fm + "depends_on:\n" for ref in $data.depends_on { $fm = $fm + $" - $ref\n" } } if ($data.implements | length) > 0 { $fm = $fm + "implements:\n" for ref in $data.implements { $fm = $fm + $" - $ref\n" } } if ($data.extends | length) > 0 { $fm = $fm + "extends:\n" for ref in $data.extends { $fm = $fm + $" - $ref\n" } } $fm + "---" } def convert_logseq_syntax [body: string] { # Phase 1: Convert Logseq task markers to standard markdown mut converted = $body | str replace -a 'LATER ' '' $converted = $converted | str replace -a 'NOW ' '' $converted = $converted | str replace -a 'WAITING ' '' $converted = $converted | str replace -a 'CANCELLED ' '' $converted }