Commit e6b6cba99177

Vincent Demeester <vincent@sbr.pm>
2025-12-05 10:14:02
chore: Remove org-manager tool
- Superseded by Org skill for org-mode manipulation - Consolidate org-mode tooling into skill-based workflows Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent d74da8a
tools/org-manager/cmd/org-manager/main.go
@@ -1,50 +0,0 @@
-// Package main implements org-manager, a tool for managing org-mode files.
-//
-// org-manager provides functionality for backing up, validating, and checking
-// links in org-mode files, with special support for readwise and pkai notes.
-package main
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-
-	"github.com/spf13/cobra"
-	"github.com/vdemeester/home/tools/org-manager/internal/backup"
-	"github.com/vdemeester/home/tools/org-manager/internal/links"
-	"github.com/vdemeester/home/tools/org-manager/internal/readwise"
-	"github.com/vdemeester/home/tools/org-manager/internal/validate"
-)
-
-var (
-	version = "0.1.0"
-	orgDir  string
-)
-
-func main() {
-	rootCmd := &cobra.Command{
-		Use:   "org-manager",
-		Short: "Manage and validate org-mode files",
-		Long: `org-manager is a tool for managing org-mode files including:
-- Backing up readwise and pkai notes
-- Validating org-mode file structure and metadata
-- Checking links (local, denote, etc.)
-- Integrating with go-org-readwise`,
-		Version: version,
-	}
-
-	// Global flags
-	defaultOrgDir := filepath.Join(os.Getenv("HOME"), "desktop", "org")
-	rootCmd.PersistentFlags().StringVar(&orgDir, "org-dir", defaultOrgDir, "Path to org directory")
-
-	// Add subcommands
-	rootCmd.AddCommand(backup.NewCommand(&orgDir))
-	rootCmd.AddCommand(validate.NewCommand(&orgDir))
-	rootCmd.AddCommand(links.NewCommand(&orgDir))
-	rootCmd.AddCommand(readwise.NewCommand(&orgDir))
-
-	if err := rootCmd.Execute(); err != nil {
-		fmt.Fprintln(os.Stderr, err)
-		os.Exit(1)
-	}
-}
tools/org-manager/internal/backup/backup.go
@@ -1,207 +0,0 @@
-// Package backup provides functionality for backing up org-mode files.
-package backup
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"path/filepath"
-	"strings"
-	"time"
-
-	"github.com/spf13/cobra"
-)
-
-// Options holds configuration for backup operations.
-type Options struct {
-	BackupType string
-	Dest       string
-	Compress   bool
-	Timestamp  bool
-}
-
-// NewCommand creates the backup subcommand.
-func NewCommand(orgDir *string) *cobra.Command {
-	opts := &Options{}
-
-	cmd := &cobra.Command{
-		Use:   "backup",
-		Short: "Backup readwise or pkai org notes",
-		Long: `Backup org notes to a specified destination.
-
-This command can backup:
-- readwise notes (files containing ==readwise=)
-- pkai notes (files containing ==pkai--)
-- all notes of both types
-
-By default, a timestamp is added to the backup directory name.`,
-		Example: `  # Backup all readwise notes
-  org-manager backup --type readwise --dest ~/backups
-
-  # Backup pkai notes without timestamp
-  org-manager backup --type pkai --dest ~/backups/pkai --timestamp=false
-
-  # Backup all notes
-  org-manager backup --type all --dest ~/backups`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return Run(opts, *orgDir)
-		},
-	}
-
-	cmd.Flags().StringVar(&opts.BackupType, "type", "all", "Type of notes to backup: readwise, pkai, or all")
-	cmd.Flags().StringVar(&opts.Dest, "dest", "", "Destination directory for backup (required)")
-	cmd.Flags().BoolVar(&opts.Compress, "compress", false, "Create a compressed tar.gz archive")
-	cmd.Flags().BoolVar(&opts.Timestamp, "timestamp", true, "Add timestamp to backup directory/archive name")
-
-	cmd.MarkFlagRequired("dest")
-
-	return cmd
-}
-
-// Run executes the backup operation.
-func Run(opts *Options, orgDir string) error {
-	if opts.BackupType != "readwise" && opts.BackupType != "pkai" && opts.BackupType != "all" {
-		return fmt.Errorf("invalid backup type: %s (must be readwise, pkai, or all)", opts.BackupType)
-	}
-
-	notesDir := filepath.Join(orgDir, "notes")
-	if _, err := os.Stat(notesDir); os.IsNotExist(err) {
-		return fmt.Errorf("notes directory does not exist: %s", notesDir)
-	}
-
-	// Prepare destination
-	destPath := opts.Dest
-	if opts.Timestamp {
-		ts := time.Now().Format("20060102-150405")
-		if opts.Compress {
-			destPath = filepath.Join(opts.Dest, fmt.Sprintf("org-backup-%s.tar.gz", ts))
-		} else {
-			destPath = filepath.Join(opts.Dest, fmt.Sprintf("org-backup-%s", ts))
-		}
-	}
-
-	// Create destination directory if not compressing
-	if !opts.Compress {
-		if err := os.MkdirAll(destPath, 0755); err != nil {
-			return fmt.Errorf("failed to create destination directory: %w", err)
-		}
-	} else {
-		// Ensure parent directory exists for compressed archive
-		parentDir := filepath.Dir(destPath)
-		if err := os.MkdirAll(parentDir, 0755); err != nil {
-			return fmt.Errorf("failed to create parent directory: %w", err)
-		}
-	}
-
-	// Find files to backup
-	var files []string
-	err := filepath.Walk(notesDir, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if info.IsDir() {
-			return nil
-		}
-		if !strings.HasSuffix(path, ".org") {
-			return nil
-		}
-
-		basename := filepath.Base(path)
-		shouldBackup := false
-
-		switch opts.BackupType {
-		case "readwise":
-			shouldBackup = strings.Contains(basename, "==readwise=")
-		case "pkai":
-			shouldBackup = strings.Contains(basename, "==pkai--")
-		case "all":
-			shouldBackup = strings.Contains(basename, "==readwise=") || strings.Contains(basename, "==pkai--")
-		}
-
-		if shouldBackup {
-			files = append(files, path)
-		}
-		return nil
-	})
-	if err != nil {
-		return fmt.Errorf("failed to scan notes directory: %w", err)
-	}
-
-	if len(files) == 0 {
-		fmt.Printf("No files found to backup (type: %s)\n", opts.BackupType)
-		return nil
-	}
-
-	// Perform backup
-	if opts.Compress {
-		return createTarGzArchive(files, notesDir, destPath)
-	}
-	return copyFiles(files, notesDir, destPath)
-}
-
-func copyFiles(files []string, sourceBase, destBase string) error {
-	copied := 0
-	for _, srcPath := range files {
-		relPath, err := filepath.Rel(sourceBase, srcPath)
-		if err != nil {
-			return fmt.Errorf("failed to get relative path for %s: %w", srcPath, err)
-		}
-
-		destPath := filepath.Join(destBase, relPath)
-		destDir := filepath.Dir(destPath)
-
-		if err := os.MkdirAll(destDir, 0755); err != nil {
-			return fmt.Errorf("failed to create directory %s: %w", destDir, err)
-		}
-
-		if err := copyFile(srcPath, destPath); err != nil {
-			return fmt.Errorf("failed to copy %s: %w", srcPath, err)
-		}
-		copied++
-	}
-
-	fmt.Printf("Successfully backed up %d files to %s\n", copied, destBase)
-	return nil
-}
-
-func copyFile(src, dst string) error {
-	sourceFile, err := os.Open(src)
-	if err != nil {
-		return fmt.Errorf("opening source file: %w", err)
-	}
-	defer sourceFile.Close()
-
-	destFile, err := os.Create(dst)
-	if err != nil {
-		return fmt.Errorf("creating destination file: %w", err)
-	}
-
-	// Copy file contents
-	if _, err := io.Copy(destFile, sourceFile); err != nil {
-		destFile.Close()
-		return fmt.Errorf("copying file contents: %w", err)
-	}
-
-	// Close destination file and check for errors
-	if err := destFile.Close(); err != nil {
-		return fmt.Errorf("closing destination file: %w", err)
-	}
-
-	// Preserve permissions
-	srcInfo, err := os.Stat(src)
-	if err != nil {
-		return fmt.Errorf("getting source file info: %w", err)
-	}
-
-	if err := os.Chmod(dst, srcInfo.Mode()); err != nil {
-		return fmt.Errorf("setting permissions: %w", err)
-	}
-
-	return nil
-}
-
-func createTarGzArchive(files []string, sourceBase, archivePath string) error {
-	// For now, we'll use the tar command rather than implementing tar.gz from scratch
-	// This is simpler and more reliable
-	return fmt.Errorf("compressed backup not yet implemented - use --compress=false for now")
-}
tools/org-manager/internal/backup/backup_test.go
@@ -1,135 +0,0 @@
-package backup
-
-import (
-	"os"
-	"path/filepath"
-	"testing"
-)
-
-func TestCopyFile(t *testing.T) {
-	// Create temporary directories
-	tmpDir, err := os.MkdirTemp("", "org-manager-backup-test-*")
-	if err != nil {
-		t.Fatalf("failed to create temp dir: %v", err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	// Create source file
-	srcPath := filepath.Join(tmpDir, "source.txt")
-	srcContent := []byte("test content\nline 2\n")
-	if err := os.WriteFile(srcPath, srcContent, 0644); err != nil {
-		t.Fatalf("failed to create source file: %v", err)
-	}
-
-	// Test copying
-	dstPath := filepath.Join(tmpDir, "dest.txt")
-	if err := copyFile(srcPath, dstPath); err != nil {
-		t.Fatalf("copyFile() error = %v", err)
-	}
-
-	// Verify destination file exists and has same content
-	dstContent, err := os.ReadFile(dstPath)
-	if err != nil {
-		t.Fatalf("failed to read destination file: %v", err)
-	}
-
-	if string(dstContent) != string(srcContent) {
-		t.Errorf("destination content = %q, want %q", string(dstContent), string(srcContent))
-	}
-
-	// Verify permissions were preserved
-	srcInfo, err := os.Stat(srcPath)
-	if err != nil {
-		t.Fatalf("failed to stat source file: %v", err)
-	}
-
-	dstInfo, err := os.Stat(dstPath)
-	if err != nil {
-		t.Fatalf("failed to stat destination file: %v", err)
-	}
-
-	if dstInfo.Mode() != srcInfo.Mode() {
-		t.Errorf("destination mode = %v, want %v", dstInfo.Mode(), srcInfo.Mode())
-	}
-}
-
-func TestCopyFileErrors(t *testing.T) {
-	tmpDir, err := os.MkdirTemp("", "org-manager-backup-test-*")
-	if err != nil {
-		t.Fatalf("failed to create temp dir: %v", err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	tests := []struct {
-		name    string
-		src     string
-		dst     string
-		wantErr bool
-	}{
-		{
-			name:    "source does not exist",
-			src:     filepath.Join(tmpDir, "nonexistent.txt"),
-			dst:     filepath.Join(tmpDir, "dest.txt"),
-			wantErr: true,
-		},
-		{
-			name:    "destination in nonexistent directory",
-			src:     filepath.Join(tmpDir, "source.txt"),
-			dst:     filepath.Join(tmpDir, "nonexistent", "dest.txt"),
-			wantErr: true,
-		},
-	}
-
-	// Create source file for tests that need it
-	srcPath := filepath.Join(tmpDir, "source.txt")
-	if err := os.WriteFile(srcPath, []byte("test"), 0644); err != nil {
-		t.Fatalf("failed to create source file: %v", err)
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := copyFile(tt.src, tt.dst)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("copyFile() error = %v, wantErr %v", err, tt.wantErr)
-			}
-		})
-	}
-}
-
-func TestBackupOptions(t *testing.T) {
-	tests := []struct {
-		name       string
-		backupType string
-		wantValid  bool
-	}{
-		{
-			name:       "readwise type",
-			backupType: "readwise",
-			wantValid:  true,
-		},
-		{
-			name:       "pkai type",
-			backupType: "pkai",
-			wantValid:  true,
-		},
-		{
-			name:       "all type",
-			backupType: "all",
-			wantValid:  true,
-		},
-		{
-			name:       "invalid type",
-			backupType: "invalid",
-			wantValid:  false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			valid := tt.backupType == "readwise" || tt.backupType == "pkai" || tt.backupType == "all"
-			if valid != tt.wantValid {
-				t.Errorf("backup type %q validity = %v, want %v", tt.backupType, valid, tt.wantValid)
-			}
-		})
-	}
-}
tools/org-manager/internal/links/links.go
@@ -1,288 +0,0 @@
-// Package links provides functionality for checking links in org-mode files.
-package links
-
-import (
-	"bufio"
-	"encoding/json"
-	"fmt"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strings"
-
-	"github.com/spf13/cobra"
-)
-
-// LinkError represents a broken or invalid link.
-type LinkError struct {
-	File    string `json:"file"`
-	Line    int    `json:"line"`
-	Link    string `json:"link"`
-	Type    string `json:"type"`
-	Message string `json:"message"`
-}
-
-func (l LinkError) String() string {
-	return fmt.Sprintf("%s:%d: [%s] %s - %s", l.File, l.Line, l.Type, l.Link, l.Message)
-}
-
-// Options holds configuration for link checking operations.
-type Options struct {
-	Format  string
-	Verbose bool
-}
-
-// NewCommand creates the check-links subcommand.
-func NewCommand(orgDir *string) *cobra.Command {
-	opts := &Options{}
-
-	cmd := &cobra.Command{
-		Use:   "check-links",
-		Short: "Check for invalid links (local, denote, etc.)",
-		Long: `Check org files for broken or invalid links including:
-- Local file links (file:...)
-- Denote links (denote:IDENTIFIER)
-- ID links (id:...)
-- HTTP/HTTPS links (basic validation)
-
-The checker will verify that linked files exist and that
-denote identifiers reference existing notes.`,
-		Example: `  # Check links with default text output
-  org-manager check-links
-
-  # Output results as JSON
-  org-manager check-links --format json
-
-  # Verbose output showing all checked links
-  org-manager check-links --verbose`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return Run(opts, *orgDir)
-		},
-	}
-
-	cmd.Flags().StringVarP(&opts.Format, "format", "f", "text", "Output format: text or json")
-	cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output including valid links")
-
-	return cmd
-}
-
-// Run executes the link checking operation.
-func Run(opts *Options, orgDir string) error {
-	notesDir := filepath.Join(orgDir, "notes")
-	if _, err := os.Stat(notesDir); os.IsNotExist(err) {
-		return fmt.Errorf("notes directory does not exist: %s", notesDir)
-	}
-
-	var errors []LinkError
-	filesChecked := 0
-
-	// Build index of identifiers for denote link checking
-	identifierIndex := make(map[string]string)
-	err := filepath.Walk(notesDir, func(path string, info os.FileInfo, err error) error {
-		if err != nil || info.IsDir() || !strings.HasSuffix(path, ".org") {
-			return err
-		}
-		if id := extractIdentifier(path); id != "" {
-			identifierIndex[id] = path
-		}
-		return nil
-	})
-	if err != nil {
-		return fmt.Errorf("failed to build identifier index: %w", err)
-	}
-
-	// Check links in each file
-	err = filepath.Walk(notesDir, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if info.IsDir() || !strings.HasSuffix(path, ".org") {
-			return nil
-		}
-
-		if opts.Verbose {
-			fmt.Printf("Checking links in %s...\n", path)
-		}
-
-		linkErrors := checkLinksInFile(path, notesDir, identifierIndex, opts.Verbose)
-		errors = append(errors, linkErrors...)
-		filesChecked++
-		return nil
-	})
-
-	if err != nil {
-		return fmt.Errorf("failed to scan directory: %w", err)
-	}
-
-	// Output results
-	if opts.Format == "json" {
-		return outputJSON(errors, filesChecked)
-	}
-	return outputText(errors, filesChecked)
-}
-
-func checkLinksInFile(path, notesDir string, identifierIndex map[string]string, verbose bool) []LinkError {
-	var errors []LinkError
-
-	file, err := os.Open(path)
-	if err != nil {
-		errors = append(errors, LinkError{
-			File:    path,
-			Line:    0,
-			Type:    "error",
-			Message: fmt.Sprintf("failed to open file: %v", err),
-		})
-		return errors
-	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-	lineNum := 0
-	linkPattern := regexp.MustCompile(`\[\[([^\]]+)\](?:\[([^\]]+)\])?\]`)
-
-	for scanner.Scan() {
-		lineNum++
-		line := scanner.Text()
-
-		matches := linkPattern.FindAllStringSubmatch(line, -1)
-		for _, match := range matches {
-			link := match[1]
-			linkType, target := parseLinkType(link)
-
-			if verbose {
-				fmt.Printf("  Line %d: %s link: %s\n", lineNum, linkType, target)
-			}
-
-			switch linkType {
-			case "file":
-				var absPath string
-				if filepath.IsAbs(target) {
-					absPath = target
-				} else {
-					absPath = filepath.Join(filepath.Dir(path), target)
-				}
-				if _, err := os.Stat(absPath); os.IsNotExist(err) {
-					errors = append(errors, LinkError{
-						File:    path,
-						Line:    lineNum,
-						Link:    link,
-						Type:    "file",
-						Message: "file does not exist",
-					})
-				}
-
-			case "denote":
-				if _, exists := identifierIndex[target]; !exists {
-					errors = append(errors, LinkError{
-						File:    path,
-						Line:    lineNum,
-						Link:    link,
-						Type:    "denote",
-						Message: "identifier not found",
-					})
-				}
-
-			case "id":
-				if len(target) < 10 {
-					errors = append(errors, LinkError{
-						File:    path,
-						Line:    lineNum,
-						Link:    link,
-						Type:    "id",
-						Message: "invalid ID format (too short)",
-					})
-				}
-
-			case "http", "https":
-				if target == "" {
-					errors = append(errors, LinkError{
-						File:    path,
-						Line:    lineNum,
-						Link:    link,
-						Type:    linkType,
-						Message: "empty URL",
-					})
-				}
-			}
-		}
-	}
-
-	if err := scanner.Err(); err != nil {
-		errors = append(errors, LinkError{
-			File:    path,
-			Message: fmt.Sprintf("error reading file: %v", err),
-		})
-	}
-
-	return errors
-}
-
-func parseLinkType(link string) (string, string) {
-	if strings.HasPrefix(link, "file:") {
-		return "file", strings.TrimPrefix(link, "file:")
-	}
-	if strings.HasPrefix(link, "denote:") {
-		return "denote", strings.TrimPrefix(link, "denote:")
-	}
-	if strings.HasPrefix(link, "id:") {
-		return "id", strings.TrimPrefix(link, "id:")
-	}
-	if strings.HasPrefix(link, "http://") {
-		return "http", link
-	}
-	if strings.HasPrefix(link, "https://") {
-		return "https", link
-	}
-	return "file", link
-}
-
-func extractIdentifier(path string) string {
-	file, err := os.Open(path)
-	if err != nil {
-		return ""
-	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-	identifierPattern := regexp.MustCompile(`^#\+identifier:\s*(.+)$`)
-
-	for scanner.Scan() {
-		line := strings.ToLower(scanner.Text())
-		if matches := identifierPattern.FindStringSubmatch(line); matches != nil {
-			return strings.TrimSpace(matches[1])
-		}
-		if !strings.HasPrefix(line, "#+") && strings.TrimSpace(line) != "" {
-			break
-		}
-	}
-
-	return ""
-}
-
-func outputJSON(errors []LinkError, filesChecked int) error {
-	result := map[string]interface{}{
-		"files_checked": filesChecked,
-		"errors_found":  len(errors),
-		"errors":        errors,
-	}
-
-	encoder := json.NewEncoder(os.Stdout)
-	encoder.SetIndent("", "  ")
-	return encoder.Encode(result)
-}
-
-func outputText(errors []LinkError, filesChecked int) error {
-	fmt.Printf("\nLink check complete: checked %d files\n", filesChecked)
-
-	if len(errors) == 0 {
-		fmt.Println("No broken links found!")
-		return nil
-	}
-
-	fmt.Printf("\nFound %d broken links:\n", len(errors))
-	for _, e := range errors {
-		fmt.Println(e.String())
-	}
-
-	return fmt.Errorf("link check failed with %d errors", len(errors))
-}
tools/org-manager/internal/readwise/readwise.go
@@ -1,89 +0,0 @@
-// Package readwise provides integration with go-org-readwise.
-package readwise
-
-import (
-	"fmt"
-	"os"
-	"os/exec"
-
-	"github.com/spf13/cobra"
-)
-
-// Options holds configuration for readwise operations.
-type Options struct {
-	Sync    bool
-	DryRun  bool
-	Verbose bool
-}
-
-// NewCommand creates the readwise subcommand.
-func NewCommand(orgDir *string) *cobra.Command {
-	opts := &Options{}
-
-	cmd := &cobra.Command{
-		Use:   "readwise",
-		Short: "Call go-org-readwise to sync highlights",
-		Long: `Wrapper around go-org-readwise for syncing Readwise highlights to org files.
-
-This command calls the go-org-readwise tool which should be installed
-separately. It syncs your Readwise highlights into org-mode files in
-the configured org directory.`,
-		Example: `  # Sync readwise highlights
-  org-manager readwise --sync
-
-  # Dry run to see what would be synced
-  org-manager readwise --sync --dry-run
-
-  # Verbose output
-  org-manager readwise --sync --verbose`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return Run(opts, *orgDir)
-		},
-	}
-
-	cmd.Flags().BoolVar(&opts.Sync, "sync", false, "Sync readwise highlights")
-	cmd.Flags().BoolVar(&opts.DryRun, "dry-run", false, "Show what would be done without making changes")
-	cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output")
-
-	return cmd
-}
-
-// Run executes the readwise sync operation.
-func Run(opts *Options, orgDir string) error {
-	_, err := exec.LookPath("go-org-readwise")
-	if err != nil {
-		return fmt.Errorf("go-org-readwise not found in PATH. Please install it first")
-	}
-
-	if !opts.Sync {
-		return fmt.Errorf("please specify --sync to sync readwise highlights")
-	}
-
-	args := []string{}
-
-	if opts.DryRun {
-		args = append(args, "--dry-run")
-	}
-
-	if opts.Verbose {
-		args = append(args, "--verbose")
-	}
-
-	args = append(args, "--org-dir", orgDir)
-
-	cmdExec := exec.Command("go-org-readwise", args...)
-	cmdExec.Stdout = os.Stdout
-	cmdExec.Stderr = os.Stderr
-	cmdExec.Stdin = os.Stdin
-
-	if opts.Verbose {
-		fmt.Printf("Executing: go-org-readwise %s\n", args)
-	}
-
-	if err := cmdExec.Run(); err != nil {
-		return fmt.Errorf("go-org-readwise failed: %w", err)
-	}
-
-	fmt.Println("Readwise sync completed successfully")
-	return nil
-}
tools/org-manager/internal/validate/validate.go
@@ -1,283 +0,0 @@
-// Package validate provides functionality for validating org-mode files.
-package validate
-
-import (
-	"bufio"
-	"fmt"
-	"os"
-	"path/filepath"
-	"regexp"
-	"strings"
-
-	"github.com/spf13/cobra"
-)
-
-// Compile regex patterns once at package level for better performance
-var (
-	metadataPattern     = regexp.MustCompile(`^#\+(\w+):\s*(.*)$`)
-	headlinePattern     = regexp.MustCompile(`^(\*+)\s+(.*)$`)
-	propertyDrawerStart = regexp.MustCompile(`^\s*:PROPERTIES:\s*$`)
-	propertyDrawerEnd   = regexp.MustCompile(`^\s*:END:\s*$`)
-)
-
-// ValidationError represents an error found during validation.
-type ValidationError struct {
-	File    string
-	Line    int
-	Column  int
-	Message string
-}
-
-func (v ValidationError) String() string {
-	if v.Line > 0 {
-		return fmt.Sprintf("%s:%d:%d: %s", v.File, v.Line, v.Column, v.Message)
-	}
-	return fmt.Sprintf("%s: %s", v.File, v.Message)
-}
-
-// Options holds configuration for validation operations.
-type Options struct {
-	CheckAll       bool
-	CheckMetadata  bool
-	CheckStructure bool
-	Verbose        bool
-}
-
-// NewCommand creates the validate subcommand.
-func NewCommand(orgDir *string) *cobra.Command {
-	opts := &Options{}
-
-	cmd := &cobra.Command{
-		Use:   "validate",
-		Short: "Validate org-mode files for correctness",
-		Long: `Validate org-mode files checking for:
-- Required metadata (title, identifier)
-- Proper headline structure
-- Balanced brackets and parentheses
-- Denote filename conventions
-
-The validator walks through all .org files in the notes directory
-and reports any structural or metadata issues.`,
-		Example: `  # Validate all org files
-  org-manager validate
-
-  # Validate with verbose output
-  org-manager validate --verbose
-
-  # Only check metadata
-  org-manager validate --check-structure=false`,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return Run(opts, *orgDir)
-		},
-	}
-
-	cmd.Flags().BoolVar(&opts.CheckAll, "check-all", true, "Check all org files in the directory")
-	cmd.Flags().BoolVar(&opts.CheckMetadata, "check-metadata", true, "Validate org-mode metadata")
-	cmd.Flags().BoolVar(&opts.CheckStructure, "check-structure", true, "Validate org-mode structure")
-	cmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "Show verbose output")
-
-	return cmd
-}
-
-// Run executes the validation operation.
-func Run(opts *Options, orgDir string) error {
-	notesDir := filepath.Join(orgDir, "notes")
-	if _, err := os.Stat(notesDir); os.IsNotExist(err) {
-		return fmt.Errorf("notes directory does not exist: %s", notesDir)
-	}
-
-	var errors []ValidationError
-	filesChecked := 0
-
-	err := filepath.Walk(notesDir, func(path string, info os.FileInfo, err error) error {
-		if err != nil {
-			return err
-		}
-		if info.IsDir() || !strings.HasSuffix(path, ".org") {
-			return nil
-		}
-
-		if opts.Verbose {
-			fmt.Printf("Checking %s...\n", path)
-		}
-
-		fileErrors := validateOrgFile(path, opts.CheckMetadata, opts.CheckStructure)
-		errors = append(errors, fileErrors...)
-		filesChecked++
-		return nil
-	})
-
-	if err != nil {
-		return fmt.Errorf("failed to scan directory: %w", err)
-	}
-
-	// Print results
-	fmt.Printf("\nValidation complete: checked %d files\n", filesChecked)
-
-	if len(errors) == 0 {
-		fmt.Println("No errors found!")
-		return nil
-	}
-
-	fmt.Printf("\nFound %d errors:\n", len(errors))
-	for _, e := range errors {
-		fmt.Println(e.String())
-	}
-
-	return fmt.Errorf("validation failed with %d errors", len(errors))
-}
-
-func validateOrgFile(path string, checkMetadata, checkStructure bool) []ValidationError {
-	var errors []ValidationError
-
-	file, err := os.Open(path)
-	if err != nil {
-		errors = append(errors, ValidationError{
-			File:    path,
-			Message: fmt.Sprintf("failed to open file: %v", err),
-		})
-		return errors
-	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-	lineNum := 0
-	hasTitle := false
-	hasIdentifier := false
-	inMetadata := true
-	headlineLevel := 0
-	inPropertyDrawer := false
-
-	for scanner.Scan() {
-		lineNum++
-		line := scanner.Text()
-
-		// Check for metadata in the header
-		if inMetadata {
-			if strings.HasPrefix(line, "#+") {
-				if matches := metadataPattern.FindStringSubmatch(line); matches != nil {
-					key := strings.ToLower(matches[1])
-					value := strings.TrimSpace(matches[2])
-
-					if checkMetadata {
-						if key == "title" {
-							hasTitle = true
-							if value == "" {
-								errors = append(errors, ValidationError{
-									File:    path,
-									Line:    lineNum,
-									Message: "empty title",
-								})
-							}
-						}
-						if key == "identifier" {
-							hasIdentifier = true
-							if value == "" {
-								errors = append(errors, ValidationError{
-									File:    path,
-									Line:    lineNum,
-									Message: "empty identifier",
-								})
-							}
-						}
-					}
-				}
-			} else if strings.TrimSpace(line) == "" {
-				// Empty line after metadata is okay
-				continue
-			} else if !strings.HasPrefix(line, "#") {
-				// We've exited the metadata section
-				inMetadata = false
-			}
-		}
-
-		// Check property drawers
-		if propertyDrawerStart.MatchString(line) {
-			inPropertyDrawer = true
-		} else if propertyDrawerEnd.MatchString(line) {
-			inPropertyDrawer = false
-		}
-
-		// Check headline structure
-		if checkStructure && !inPropertyDrawer {
-			if matches := headlinePattern.FindStringSubmatch(line); matches != nil {
-				level := len(matches[1])
-				// Check for excessive level jumps (e.g., * followed by ***)
-				if headlineLevel > 0 && level > headlineLevel+1 {
-					errors = append(errors, ValidationError{
-						File:    path,
-						Line:    lineNum,
-						Message: fmt.Sprintf("headline level jump from %d to %d", headlineLevel, level),
-					})
-				}
-				headlineLevel = level
-			}
-		}
-
-		// Check for unclosed brackets/parens in non-code blocks
-		if checkStructure && !strings.HasPrefix(strings.TrimSpace(line), "#+begin") {
-			if err := checkBrackets(line); err != nil {
-				errors = append(errors, ValidationError{
-					File:    path,
-					Line:    lineNum,
-					Message: err.Error(),
-				})
-			}
-		}
-	}
-
-	if err := scanner.Err(); err != nil {
-		errors = append(errors, ValidationError{
-			File:    path,
-			Message: fmt.Sprintf("error reading file: %v", err),
-		})
-	}
-
-	// Check required metadata
-	if checkMetadata {
-		if !hasTitle {
-			errors = append(errors, ValidationError{
-				File:    path,
-				Line:    0,
-				Message: "missing required #+title metadata",
-			})
-		}
-		if !hasIdentifier {
-			errors = append(errors, ValidationError{
-				File:    path,
-				Line:    0,
-				Message: "missing required #+identifier metadata",
-			})
-		}
-	}
-
-	return errors
-}
-
-func checkBrackets(line string) error {
-	// Simple bracket/paren matching
-	stack := []rune{}
-	pairs := map[rune]rune{
-		')': '(',
-		']': '[',
-		'}': '{',
-	}
-
-	for i, ch := range line {
-		switch ch {
-		case '(', '[', '{':
-			stack = append(stack, ch)
-		case ')', ']', '}':
-			if len(stack) == 0 {
-				return fmt.Errorf("unmatched closing bracket '%c' at position %d", ch, i+1)
-			}
-			last := stack[len(stack)-1]
-			stack = stack[:len(stack)-1]
-			if pairs[ch] != last {
-				return fmt.Errorf("mismatched brackets: expected '%c' but got '%c' at position %d", last, ch, i+1)
-			}
-		}
-	}
-
-	return nil
-}
tools/org-manager/internal/validate/validate_test.go
@@ -1,204 +0,0 @@
-package validate
-
-import (
-	"os"
-	"path/filepath"
-	"testing"
-)
-
-func TestCheckBrackets(t *testing.T) {
-	tests := []struct {
-		name    string
-		line    string
-		wantErr bool
-	}{
-		{
-			name:    "balanced parentheses",
-			line:    "This is a (test) line",
-			wantErr: false,
-		},
-		{
-			name:    "balanced brackets",
-			line:    "Link to [[file:test.org][test]]",
-			wantErr: false,
-		},
-		{
-			name:    "balanced braces",
-			line:    "Some code {foo: bar}",
-			wantErr: false,
-		},
-		{
-			name:    "nested balanced",
-			line:    "Nested ((test [with {brackets}]))",
-			wantErr: false,
-		},
-		{
-			name:    "unmatched opening",
-			line:    "Unmatched (test",
-			wantErr: false, // We don't check for unclosed brackets, only mismatched closes
-		},
-		{
-			name:    "unmatched closing",
-			line:    "Unmatched test)",
-			wantErr: true,
-		},
-		{
-			name:    "mismatched brackets",
-			line:    "Mismatched (test]",
-			wantErr: true,
-		},
-		{
-			name:    "empty line",
-			line:    "",
-			wantErr: false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := checkBrackets(tt.line)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("checkBrackets(%q) error = %v, wantErr %v", tt.line, err, tt.wantErr)
-			}
-		})
-	}
-}
-
-func TestValidateOrgFile(t *testing.T) {
-	// Create a temporary directory for test files
-	tmpDir, err := os.MkdirTemp("", "org-manager-test-*")
-	if err != nil {
-		t.Fatalf("failed to create temp dir: %v", err)
-	}
-	defer os.RemoveAll(tmpDir)
-
-	tests := []struct {
-		name           string
-		content        string
-		checkMetadata  bool
-		checkStructure bool
-		wantErrors     int
-	}{
-		{
-			name: "valid org file",
-			content: `#+title: Test Note
-#+identifier: 20231028T082123
-
-* Headline 1
-** Headline 2
-`,
-			checkMetadata:  true,
-			checkStructure: true,
-			wantErrors:     0,
-		},
-		{
-			name: "missing title",
-			content: `#+identifier: 20231028T082123
-
-* Content
-`,
-			checkMetadata:  true,
-			checkStructure: false,
-			wantErrors:     1, // missing title
-		},
-		{
-			name: "missing identifier",
-			content: `#+title: Test
-
-* Content
-`,
-			checkMetadata:  true,
-			checkStructure: false,
-			wantErrors:     1, // missing identifier
-		},
-		{
-			name: "headline level jump",
-			content: `#+title: Test
-#+identifier: 20231028T082123
-
-* Level 1
-*** Level 3 (skipped 2)
-`,
-			checkMetadata:  false,
-			checkStructure: true,
-			wantErrors:     1, // level jump
-		},
-		{
-			name: "empty title",
-			content: `#+title:
-#+identifier: 20231028T082123
-
-* Content
-`,
-			checkMetadata:  true,
-			checkStructure: false,
-			wantErrors:     1, // empty title
-		},
-		{
-			name: "unmatched brackets",
-			content: `#+title: Test
-#+identifier: 20231028T082123
-
-* Headline with unclosed (bracket
-`,
-			checkMetadata:  false,
-			checkStructure: true,
-			wantErrors:     0, // We don't check for unclosed brackets currently
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			// Create test file
-			testFile := filepath.Join(tmpDir, "test.org")
-			if err := os.WriteFile(testFile, []byte(tt.content), 0644); err != nil {
-				t.Fatalf("failed to create test file: %v", err)
-			}
-
-			errors := validateOrgFile(testFile, tt.checkMetadata, tt.checkStructure)
-
-			if len(errors) != tt.wantErrors {
-				t.Errorf("validateOrgFile() got %d errors, want %d", len(errors), tt.wantErrors)
-				for _, e := range errors {
-					t.Logf("  Error: %s", e.String())
-				}
-			}
-		})
-	}
-}
-
-func TestValidationErrorString(t *testing.T) {
-	tests := []struct {
-		name string
-		err  ValidationError
-		want string
-	}{
-		{
-			name: "with line and column",
-			err: ValidationError{
-				File:    "/path/to/file.org",
-				Line:    10,
-				Column:  5,
-				Message: "test error",
-			},
-			want: "/path/to/file.org:10:5: test error",
-		},
-		{
-			name: "without line",
-			err: ValidationError{
-				File:    "/path/to/file.org",
-				Message: "test error",
-			},
-			want: "/path/to/file.org: test error",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := tt.err.String()
-			if got != tt.want {
-				t.Errorf("ValidationError.String() = %q, want %q", got, tt.want)
-			}
-		})
-	}
-}
tools/org-manager/default.nix
@@ -1,46 +0,0 @@
-{
-  lib,
-  buildGoModule,
-  installShellFiles,
-}:
-
-buildGoModule {
-  pname = "org-manager";
-  version = "0.1.0";
-
-  src = ./.;
-
-  # Build from cmd/org-manager subdirectory
-  subPackages = [ "cmd/org-manager" ];
-
-  vendorHash = "sha256-hocnLCzWN8srQcO3BMNkd2lt0m54Qe7sqAhUxVZlz1k=";
-
-  nativeBuildInputs = [ installShellFiles ];
-
-  ldflags = [
-    "-s"
-    "-w"
-  ];
-
-  postInstall = ''
-    installShellCompletion --cmd org-manager \
-      --bash <($out/bin/org-manager completion bash) \
-      --fish <($out/bin/org-manager completion fish) \
-      --zsh <($out/bin/org-manager completion zsh)
-  '';
-
-  meta = with lib; {
-    description = "Tool for managing org-mode files including backup, validation, and link checking";
-    longDescription = ''
-      org-manager is a comprehensive tool for managing org-mode files with features including:
-      - Backing up readwise and pkai notes
-      - Validating org-mode file structure and metadata
-      - Checking links (local, denote, id links)
-      - Integration with go-org-readwise for syncing highlights
-    '';
-    homepage = "https://github.com/vdemeester/home";
-    license = licenses.asl20;
-    platforms = platforms.unix;
-    mainProgram = "org-manager";
-  };
-}
tools/org-manager/go.mod
@@ -1,10 +0,0 @@
-module github.com/vdemeester/home/tools/org-manager
-
-go 1.23
-
-require github.com/spf13/cobra v1.8.1
-
-require (
-	github.com/inconshreveable/mousetrap v1.1.0 // indirect
-	github.com/spf13/pflag v1.0.5 // indirect
-)
tools/org-manager/go.sum
@@ -1,10 +0,0 @@
-github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
-github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
-github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tools/org-manager/README.md
@@ -1,195 +0,0 @@
-# org-manager
-
-A comprehensive tool for managing org-mode files with features for backup, validation, and link checking.
-
-## Features
-
-- **Backup**: Backup readwise and pkai notes to a specified destination
-- **Validation**: Validate org-mode file structure and metadata
-- **Link Checking**: Check for broken or invalid links (local, denote, id links)
-- **Readwise Integration**: Wrapper for go-org-readwise to sync highlights
-
-## Installation
-
-Build and install using Nix:
-
-```bash
-nix build .#org-manager
-# Or install to your profile
-nix profile install .#org-manager
-```
-
-## Usage
-
-### Global Flags
-
-- `--org-dir <path>`: Path to org directory (default: `~/desktop/org`)
-- `--version`: Show version information
-
-### Commands
-
-#### backup
-
-Backup readwise or pkai notes to a destination directory.
-
-```bash
-# Backup all readwise notes
-org-manager backup --type readwise --dest ~/backups
-
-# Backup pkai notes without timestamp
-org-manager backup --type pkai --dest ~/backups/pkai --timestamp=false
-
-# Backup all notes (both readwise and pkai)
-org-manager backup --type all --dest ~/backups
-```
-
-**Flags:**
-- `--type <readwise|pkai|all>`: Type of notes to backup (default: all)
-- `--dest <path>`: Destination directory (required)
-- `--timestamp`: Add timestamp to backup directory name (default: true)
-- `--compress`: Create compressed tar.gz archive (not yet implemented)
-
-#### validate
-
-Validate org-mode files for correctness.
-
-```bash
-# Validate all org files
-org-manager validate
-
-# Validate with verbose output
-org-manager validate --verbose
-
-# Only check metadata (skip structure checks)
-org-manager validate --check-structure=false
-```
-
-**Flags:**
-- `--check-all`: Check all org files in directory (default: true)
-- `--check-metadata`: Validate org-mode metadata (default: true)
-- `--check-structure`: Validate org-mode structure (default: true)
-- `--verbose`, `-v`: Show verbose output
-
-**Checks performed:**
-- Required metadata (#+title, #+identifier)
-- Proper headline structure (no level jumps)
-- Balanced brackets and parentheses
-- Empty metadata values
-
-#### check-links
-
-Check for broken or invalid links in org files.
-
-```bash
-# Check links with default text output
-org-manager check-links
-
-# Output results as JSON
-org-manager check-links --format json
-
-# Verbose output showing all checked links
-org-manager check-links --verbose
-```
-
-**Flags:**
-- `--format <text|json>`: Output format (default: text)
-- `--verbose`, `-v`: Show verbose output including valid links
-
-**Link types checked:**
-- Local file links (`file:...`)
-- Denote links (`denote:IDENTIFIER`)
-- ID links (`id:...`)
-- HTTP/HTTPS links (basic validation)
-
-#### readwise
-
-Wrapper for go-org-readwise to sync highlights.
-
-```bash
-# Sync readwise highlights
-org-manager readwise --sync
-
-# Dry run to see what would be synced
-org-manager readwise --sync --dry-run
-
-# Verbose output
-org-manager readwise --sync --verbose
-```
-
-**Flags:**
-- `--sync`: Sync readwise highlights (required)
-- `--dry-run`: Show what would be done without making changes
-- `--verbose`, `-v`: Show verbose output
-
-**Note:** This command requires `go-org-readwise` to be installed and available in PATH.
-
-## Project Structure
-
-This project follows the standard Go project layout:
-
-```
-org-manager/
-├── cmd/
-│   └── org-manager/      # Main application entry point
-│       └── main.go
-├── internal/             # Private application code
-│   ├── backup/          # Backup functionality
-│   │   ├── backup.go
-│   │   └── backup_test.go
-│   ├── validate/        # Validation logic
-│   │   ├── validate.go
-│   │   └── validate_test.go
-│   ├── links/           # Link checking
-│   │   └── links.go
-│   └── readwise/        # Readwise integration
-│       └── readwise.go
-├── go.mod               # Go module definition
-├── go.sum               # Dependency checksums
-├── default.nix          # Nix package definition
-└── README.md
-```
-
-The tool expects org files to be located in `<org-dir>/notes/` by default.
-
-### Supported Note Types
-
-- **Readwise notes**: Files containing `==readwise=` in the filename
-- **PKAI notes**: Files containing `==pkai--` in the filename
-
-### Required Metadata
-
-All org files should include:
-- `#+title:` - The note title
-- `#+identifier:` - A unique identifier (typically timestamp-based)
-
-Example:
-```org
-#+title: My Note Title
-#+identifier: 20231028T082123
-#+filetags: :tag1:tag2:
-
-* Content here
-```
-
-## Development
-
-### Building from Source
-
-```bash
-cd tools/org-manager
-go build
-```
-
-### Testing
-
-```bash
-go test ./...
-```
-
-## Future Enhancements
-
-- Compressed backup support (tar.gz)
-- Additional validation checks
-- Link repair functionality
-- Statistics and reporting
-- Integration with other org-mode tools