Skill: shad0wca7/apple-notes-applescript (version 1.1.0)

--- name: apple-notes-custom description: Apple Notes.app integration for macOS. List folders, read, create, search, edit, and delete notes via AppleScript. metadata: {"clawdbot":{"emoji":"📝","os":["darwin"]}} --- # Apple Notes Interact with Notes.app via AppleScript. Run scripts from: `cd {baseDir}` ## Commands | Command | Usage | |---------|-------| | List folders | `scripts/notes-folders.sh [--tree] [--counts]` | | List notes | `scripts/notes-list.sh [folder] [limit]` | | Read note | `scripts/notes-read.sh <name-or-id> [folder]` | | Create note | `scripts/notes-create.sh <folder> <title> [body]` | | Search notes | `scripts/notes-search.sh <query> [folder] [limit] [--title-only]` | | Edit note | `scripts/notes-edit.sh <name-or-id> <new-body> [folder]` | | Delete note | `scripts/notes-delete.sh <name> <folder>` ⚠️ folder required | ## Folder Paths All commands support subfolder paths with `/` separator: ```bash scripts/notes-list.sh "Scanned/Medical & Health" 10 scripts/notes-read.sh "blood test" "Scanned/Medical & Health" scripts/notes-create.sh "Property/416 Garfield" "Inspection notes" "Roof looks good" ``` ### Folder Tree Structure This collection has 4000+ notes. Key structure: - **Scanned** — parent folder with many subfolders (Medical & Health, Receipts, etc.) - **Fetish** — parent with subfolders (AW, Bimbo, Events, etc.) - **Hobbies** — parent with subfolders (3d printing, Homelab, etc.) - **Property** — subfolders per address Use `--tree --counts` to see the full hierarchy. ## Folder Listing ```bash scripts/notes-folders.sh # Flat list scripts/notes-folders.sh --counts # With note counts scripts/notes-folders.sh --tree --counts # Full hierarchy with counts ``` ## Listing Notes ```bash scripts/notes-list.sh "Notes" 10 # Specific folder scripts/notes-list.sh "Scanned/Receipts" 5 # Subfolder scripts/notes-list.sh "" 10 # All folders (shows folder name per note) ``` Without a folder, output includes the folder column: `ID | Date | Folder | Title` With a folder: `ID | Date | Title` ## Reading Notes ```bash scripts/notes-read.sh "blood test" "Scanned/Medical & Health" # By name (partial match) scripts/notes-read.sh "x-coredata://…/ICNote/p12345" # By ID (direct lookup, fast) ``` Output: Title, Folder, Modified date, ID, then body text. ## Searching Title search first (fast), body search fallback (slower): ```bash scripts/notes-search.sh "tax" "" 10 # All folders scripts/notes-search.sh "receipt" "Scanned/Receipts" 5 # Specific folder scripts/notes-search.sh "keyword" "" 10 --title-only # Skip body search ``` Output: `ID | Date | Folder | Title` ## Creating Notes ```bash scripts/notes-create.sh "Notes" "My Title" "Body text here" # With body scripts/notes-create.sh "Notes" "Empty Note" # Title only ``` Returns the created note's ID. ## Editing Notes ```bash scripts/notes-edit.sh "My Note" "New body content" "Notes" # By name scripts/notes-edit.sh "x-coredata://…/ICNote/p12345" "New body" # By ID ``` ## Deleting Notes ```bash scripts/notes-delete.sh "Old Note" "Notes" # Folder required scripts/notes-delete.sh "receipt" "Scanned/Receipts" ``` ⚠️ Folder argument is **required** for safety — prevents accidental matches across 4000+ notes. ## Performance Tips | Situation | Tip | |-----------|-----| | Listing/searching all notes | **Always specify a folder** — iterating 4000+ notes is slow | | Reading a known note | Use the **ID** from a previous list/search — instant lookup | | Searching large folders | Use `--title-only` if body search isn't needed | | Finding the right folder | Use `--tree --counts` first to see hierarchy | ## Errors | Error | Cause | |-------|-------| | `Error: Can't get folder` | Folder name doesn't exist or wrong path | | `No note matching…` | No partial match found in scope | | Empty body text | Scanned/image-only notes have no extractable text | ## Technical Notes - Partial name matching for read/edit/delete (first match wins) - Multiline body supported via temp files - Folder names are case-sensitive - All user inputs escaped for AppleScript safety (quotes, backslashes) - `number of` used instead of `count of` (AppleScript reserved word)

path: scripts/_resolve_folder.sh
content: #!/bin/bash
# Shared folder resolution helper
# Splits on "/" only when NOT surrounded by spaces (path separator vs folder name)
# Usage: source this, then call resolve_folder "$FOLDER"
# Sets FOLDER_SCRIPT variable

escape_as() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }

resolve_folder() {
    local folder="$1"
    if [ -z "$folder" ]; then
        FOLDER_SCRIPT="set noteList to every note"
        return
    fi
    
    # Split on "/" but only when not surrounded by spaces
    # "Scanned/Finance / Tax" → ["Scanned", "Finance / Tax"]
    # "Scanned/Medical & Health" → ["Scanned", "Medical & Health"]
    local parts=()
    local current=""
    local len=${#folder}
    local i=0
    
    while [ $i -lt $len ]; do
        local char="${folder:$i:1}"
        if [ "$char" = "/" ]; then
            # Check if surrounded by spaces (part of folder name) or path separator
            local prev_char=""
            local next_char=""
            [ $i -gt 0 ] && prev_char="${folder:$((i-1)):1}"
            [ $i -lt $((len-1)) ] && next_char="${folder:$((i+1)):1}"
            
            if [ "$prev_char" = " " ] || [ "$next_char" = " " ]; then
                # Part of folder name (e.g. "Finance / Tax")
                current="${current}${char}"
            else
                # Path separator
                parts+=("$current")
                current=""
            fi
        else
            current="${current}${char}"
        fi
        i=$((i+1))
    done
    [ -n "$current" ] && parts+=("$current")
    
    FOLDER_SCRIPT="set targetFolder to folder \"$(escape_as "${parts[0]}")\""
    for ((i=1; i<${#parts[@]}; i++)); do
        FOLDER_SCRIPT="$FOLDER_SCRIPT
        set targetFolder to folder \"$(escape_as "${parts[$i]}")\" of targetFolder"
    done
    FOLDER_SCRIPT="$FOLDER_SCRIPT
        set noteList to notes of targetFolder"
}


path: scripts/notes-attachment.sh
content: #!/bin/bash
# Extract attachments from a note (scanned documents/images)
# Usage: notes-attachment.sh <note-name> [folder] [output-dir]
# Default output-dir: /tmp/notes-export/

source "$(dirname "$0")/_resolve_folder.sh"

NAME="${1:-}"
FOLDER="${2:-}"
OUTPUT_DIR="${3:-/tmp/notes-export/}"

if [ -z "$NAME" ]; then
    echo "Usage: notes-attachment.sh <note-name> [folder] [output-dir]"
    exit 1
fi

# Auto-detect Notes account UUID
ACCOUNTS_DIR="$HOME/Library/Group Containers/group.com.apple.notes/Accounts/"
if [ ! -d "$ACCOUNTS_DIR" ]; then
    echo "Error: Notes accounts directory not found" >&2
    exit 1
fi

ACCOUNT_UUID=$(ls -1 "$ACCOUNTS_DIR" 2>/dev/null | grep -iE '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' | head -1)
if [ -z "$ACCOUNT_UUID" ]; then
    echo "Error: Could not auto-detect Notes account UUID" >&2
    exit 1
fi

FALLBACK_BASE="$ACCOUNTS_DIR/$ACCOUNT_UUID/FallbackPDFs"
PREVIEW_BASE="$ACCOUNTS_DIR/$ACCOUNT_UUID/Previews"

ESCAPED_NAME="$(escape_as "$NAME")"

# Create output directory
mkdir -p "$OUTPUT_DIR"

# ID lookup (direct)
if [[ "$NAME" == x-coredata* ]]; then
    ATTACHMENT_INFO=$(osascript <<EOF
tell application "Notes"
    try
        set n to note id "$ESCAPED_NAME"
        set noteTitle to name of n
        set noteFolder to "unknown"
        try
            set noteFolder to name of container of n
        end try
        set attachmentList to {}
        repeat with a in attachments of n
            try
                set contentId to content identifier of a
                set attachmentList to attachmentList & {contentId}
            end try
        end repeat
        return noteTitle & "|" & noteFolder & "|" & (count of attachmentList) & "|" & (attachmentList as string)
    on error errMsg
        return "ERROR:" & errMsg
    end try
end tell
EOF
)
else
    # Name lookup with folder
    resolve_folder "$FOLDER"
    
    ATTACHMENT_INFO=$(osascript <<EOF
tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "ERROR:" & errMsg
    end try
    
    set searchTerm to "$ESCAPED_NAME" as text
    repeat with n in noteList
        if name of n contains searchTerm then
            set noteTitle to name of n
            set noteFolder to "unknown"
            try
                set noteFolder to name of container of n
            end try
            set attachmentList to {}
            repeat with a in attachments of n
                try
                    set contentId to content identifier of a
                    set attachmentList to attachmentList & {contentId}
                end try
            end repeat
            return noteTitle & "|" & noteFolder & "|" & (count of attachmentList) & "|" & (attachmentList as string)
        end if
    end repeat
    return "ERROR:No note matching '" & searchTerm & "' found"
end tell
EOF
)
fi

if [[ "$ATTACHMENT_INFO" == ERROR:* ]]; then
    echo "Error: ${ATTACHMENT_INFO#ERROR:}" >&2
    exit 1
fi

# Parse attachment info
IFS='|' read -r NOTE_TITLE NOTE_FOLDER ATTACHMENT_COUNT ATTACHMENT_IDS <<< "$ATTACHMENT_INFO"

echo "Note: $NOTE_TITLE"
echo "Folder: $NOTE_FOLDER"
echo "Attachments found: $ATTACHMENT_COUNT"
echo "---"

if [ "$ATTACHMENT_COUNT" -eq 0 ]; then
    echo "No attachments found in this note."
    exit 0
fi

# Convert attachment IDs to array (comma-separated from AppleScript)
IFS=',' read -ra ID_ARRAY <<< "$ATTACHMENT_IDS"

EXTRACTED_COUNT=0

for content_id in "${ID_ARRAY[@]}"; do
    # Trim whitespace
    content_id=$(echo "$content_id" | xargs)
    # Strip cid: prefix and @icloud.apple.com suffix
    content_id=$(echo "$content_id" | sed 's/^cid://; s/@.*$//')
    
    # Clean up content ID for filename (remove UUID-like parts)
    clean_name=$(echo "$content_id" | sed 's/^[0-9A-F]\{8\}-[0-9A-F]\{4\}-[0-9A-F]\{4\}-[0-9A-F]\{4\}-[0-9A-F]\{12\}\///; s/\//-/g')
    
    # Try to find FallbackPDF first (scanned documents)
    FOUND_FILE=""
    
    # Check for FallbackPDF
    if [ -d "$FALLBACK_BASE/$content_id" ]; then
        pdf_path=$(find "$FALLBACK_BASE/$content_id" -name "FallbackPDF.pdf" 2>/dev/null | head -1)
        if [ -n "$pdf_path" ] && [ -f "$pdf_path" ]; then
            FOUND_FILE="$pdf_path"
            ext="pdf"
        fi
    fi
    
    # Look for preview images if no PDF
    if [ -z "$FOUND_FILE" ] && [ -d "$PREVIEW_BASE" ]; then
        preview_files=$(find "$PREVIEW_BASE" -name "$content_id-*.png" 2>/dev/null)
        if [ -n "$preview_files" ]; then
            # Get the largest preview file
            FOUND_FILE=$(ls -S $preview_files 2>/dev/null | head -1)
            ext="png"
        fi
    fi
    
    if [ -n "$FOUND_FILE" ]; then
        # Create sanitized filename
        safe_title=$(echo "$NOTE_TITLE" | tr -cd '[:alnum:]._-' | cut -c1-50)
        output_filename="${safe_title}-${EXTRACTED_COUNT}.${ext}"
        output_path="$OUTPUT_DIR/$output_filename"
        
        # Handle duplicates
        counter=1
        while [ -f "$output_path" ]; do
            output_filename="${safe_title}-${EXTRACTED_COUNT}(${counter}).${ext}"
            output_path="$OUTPUT_DIR/$output_filename"
            counter=$((counter + 1))
        done
        
        cp "$FOUND_FILE" "$output_path"
        echo "Extracted: $output_path"
        EXTRACTED_COUNT=$((EXTRACTED_COUNT + 1))
    else
        echo "Warning: Could not locate file for attachment: $content_id" >&2
    fi
done

echo "---"
echo "Total extracted: $EXTRACTED_COUNT"


path: scripts/notes-create.sh
content: #!/bin/bash
# Create a new note
# Usage: notes-create.sh <folder> <title> [body]
# Folder supports / paths. Body is optional (empty note with just title).

source "$(dirname "$0")/_resolve_folder.sh"

FOLDER="${1:-}"
TITLE="${2:-}"
BODY="${3:-}"

if [ -z "$FOLDER" ] || [ -z "$TITLE" ]; then
    echo "Usage: notes-create.sh <folder> <title> [body]"
    exit 1
fi

resolve_folder "$FOLDER"

# For create, we don't need "set noteList to notes of targetFolder"
# Remove the last line from FOLDER_SCRIPT
FOLDER_SCRIPT="${FOLDER_SCRIPT%
        set noteList to notes of targetFolder}"

ESCAPED_TITLE="$(escape_as "$TITLE")"

TMPFILE=$(mktemp /tmp/notes-create.XXXXXX)
printf '%s' "$BODY" > "$TMPFILE"
trap "rm -f '$TMPFILE'" EXIT

osascript <<EOF
set bodyFile to POSIX file "$TMPFILE"
set bodyText to read bodyFile as «class utf8»

tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "Error: " & errMsg
    end try
    
    if bodyText is "" then
        set htmlBody to "<h1>$ESCAPED_TITLE</h1>"
    else
        set htmlBody to "<h1>$ESCAPED_TITLE</h1><br>" & bodyText
    end if
    set newNote to make new note at targetFolder with properties {name:"$ESCAPED_TITLE", body:htmlBody}
    set newId to id of newNote
    return "Created: $ESCAPED_TITLE" & linefeed & "Folder: $FOLDER" & linefeed & "ID: " & newId
end tell
EOF


path: scripts/notes-delete.sh
content: #!/bin/bash
# Delete a note by name (partial match)
# Usage: notes-delete.sh <note-name> <folder>
# Folder is REQUIRED for safety (avoids searching 4000+ notes)

source "$(dirname "$0")/_resolve_folder.sh"

NAME="${1:-}"
FOLDER="${2:-}"

if [ -z "$NAME" ] || [ -z "$FOLDER" ]; then
    echo "Usage: notes-delete.sh <note-name> <folder>"
    echo "Folder is required for safety."
    exit 1
fi

ESCAPED_NAME="$(escape_as "$NAME")"

resolve_folder "$FOLDER"

osascript <<EOF
tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "Error: " & errMsg
    end try
    
    set searchTerm to "$ESCAPED_NAME" as text
    repeat with n in noteList
        if name of n contains searchTerm then
            set noteTitle to name of n
            try
                set noteFolder to name of container of n
            on error
                set noteFolder to "unknown"
            end try
            delete n
            return "Deleted: " & noteTitle & " (from " & noteFolder & ")"
        end if
    end repeat
    return "Error: No note matching '" & searchTerm & "' found in $FOLDER"
end tell
EOF


path: scripts/notes-edit.sh
content: #!/bin/bash
# Replace a note's body
# Usage: notes-edit.sh <note-name-or-id> <new-body> [folder]
# If name starts with "x-coredata" it's treated as ID lookup

source "$(dirname "$0")/_resolve_folder.sh"

NAME="${1:-}"
BODY="${2:-}"
FOLDER="${3:-}"

if [ -z "$NAME" ] || [ -z "$BODY" ]; then
    echo "Usage: notes-edit.sh <note-name-or-id> <new-body> [folder]"
    exit 1
fi

ESCAPED_NAME="$(escape_as "$NAME")"

TMPFILE=$(mktemp /tmp/notes-edit.XXXXXX)
printf '%s' "$BODY" > "$TMPFILE"
trap "rm -f '$TMPFILE'" EXIT

# ID lookup — direct
if [[ "$NAME" == x-coredata* ]]; then
    osascript <<EOF
set bodyFile to POSIX file "$TMPFILE"
set bodyText to read bodyFile as «class utf8»

tell application "Notes"
    try
        set n to note id "$ESCAPED_NAME"
        set noteTitle to name of n
        set body of n to "<h1>" & noteTitle & "</h1><br>" & bodyText
        return "Updated: " & noteTitle
    on error errMsg
        return "Error: " & errMsg
    end try
end tell
EOF
    exit $?
fi

# Name lookup
resolve_folder "$FOLDER"

osascript <<EOF
set bodyFile to POSIX file "$TMPFILE"
set bodyText to read bodyFile as «class utf8»

tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "Error: " & errMsg
    end try
    
    set searchTerm to "$ESCAPED_NAME" as text
    repeat with n in noteList
        if name of n contains searchTerm then
            set noteTitle to name of n
            set body of n to "<h1>" & noteTitle & "</h1><br>" & bodyText
            return "Updated: " & noteTitle
        end if
    end repeat
    return "Error: No note matching '" & searchTerm & "' found"
end tell
EOF


path: scripts/notes-export-pdf.sh
content: #!/bin/bash
# Export PDF from a note (convenience wrapper for attachment extraction)
# Usage: notes-export-pdf.sh <note-name> [folder]
# Outputs just the path to the extracted PDF for easy piping

source "$(dirname "$0")/_resolve_folder.sh"

NAME="${1:-}"
FOLDER="${2:-}"
OUTPUT_DIR="/tmp/notes-export/"

if [ -z "$NAME" ]; then
    echo "Usage: notes-export-pdf.sh <note-name> [folder]"
    exit 1
fi

# Run attachment extraction silently
"$(dirname "$0")/notes-attachment.sh" "$NAME" "$FOLDER" "$OUTPUT_DIR" > /dev/null 2>&1

if [ $? -ne 0 ]; then
    exit 1
fi

# Find the PDF file in output directory
PDF_FILE=$(ls -t "$OUTPUT_DIR"/*.pdf 2>/dev/null | head -1)

if [ -n "$PDF_FILE" ]; then
    echo "$PDF_FILE"
else
    echo "Error: No PDF found for note '$NAME'" >&2
    exit 1
fi


path: scripts/notes-folders.sh
content: #!/bin/bash
# List all Notes folders
# Usage: notes-folders.sh [--tree] [--counts]

show_tree=false
show_counts=false
for arg in "$@"; do
    case "$arg" in
        --tree) show_tree=true ;;
        --counts) show_counts=true ;;
    esac
done

if [ "$show_tree" = "true" ]; then
    osascript <<'EOF'
on listFolders(parentFolder, indent)
    set output to ""
    tell application "Notes"
        set subfolders to every folder of parentFolder
        repeat with f in subfolders
            set fname to name of f
            set c to number of notes of f
            set output to output & indent & fname & " (" & c & ")" & linefeed
        end repeat
    end tell
    repeat with f in subfolders
        set output to output & my listFolders(f, indent & "  ")
    end repeat
    return output
end listFolders

tell application "Notes"
    set output to ""
    repeat with acct in every account
        set acctName to name of acct
        set output to output & "[" & acctName & "]" & linefeed
        set topFolders to every folder of acct
        repeat with f in topFolders
            set fname to name of f
            set c to number of notes of f
            set output to output & "  " & fname & " (" & c & ")" & linefeed
            set output to output & my listFolders(f, "    ")
        end repeat
    end repeat
    return output
end tell
EOF
elif [ "$show_counts" = "true" ]; then
    osascript <<'EOF'
tell application "Notes"
    set output to ""
    repeat with f in every folder
        set fname to name of f
        set c to number of notes of f
        set output to output & fname & " (" & c & ")" & linefeed
    end repeat
    return output
end tell
EOF
else
    osascript -e 'tell application "Notes" to get name of every folder' | tr ',' '\n' | sed 's/^ //'
fi


path: scripts/notes-list.sh
content: #!/bin/bash
# List notes (ID | Date | Title)
# Usage: notes-list.sh [folder] [limit]
# Folder supports / paths: "Scanned/Medical & Health"
# Without folder, defaults to limit 20 with warning

source "$(dirname "$0")/_resolve_folder.sh"

FOLDER="${1:-}"
LIMIT="${2:-20}"

resolve_folder "$FOLDER"

if [ -z "$FOLDER" ]; then
    echo "⚠ No folder specified — listing up to $LIMIT notes from all folders (may be slow on large collections)" >&2
    SHOW_FOLDER="true"
else
    SHOW_FOLDER="false"
fi

osascript <<EOF
tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "Error: " & errMsg
    end try
    
    set maxCount to $LIMIT as integer
    set showFolder to $SHOW_FOLDER as boolean
    set output to ""
    set i to 0
    repeat with n in noteList
        if i ≥ maxCount then exit repeat
        set noteId to id of n
        set noteDate to modification date of n
        set noteTitle to name of n
        set y to year of noteDate
        set m to (month of noteDate as integer)
        set d to day of noteDate
        set dateStr to (y as text) & "-" & text -2 thru -1 of ("0" & m) & "-" & text -2 thru -1 of ("0" & d)
        if showFolder then
            try
                set fName to name of container of n
            on error
                set fName to "?"
            end try
            set output to output & noteId & " | " & dateStr & " | " & fName & " | " & noteTitle & linefeed
        else
            set output to output & noteId & " | " & dateStr & " | " & noteTitle & linefeed
        end if
        set i to i + 1
    end repeat
    if output is "" then return "No notes found"
    return output
end tell
EOF


path: scripts/notes-read.sh
content: #!/bin/bash
# Read a note by name (partial match) or by ID
# Usage: notes-read.sh <note-name-or-id> [folder]
# If name starts with "x-coredata" it's treated as an ID lookup
# Shows attachment info at bottom if attachments exist

source "$(dirname "$0")/_resolve_folder.sh"

NAME="${1:-}"
FOLDER="${2:-}"

if [ -z "$NAME" ]; then
    echo "Usage: notes-read.sh <note-name-or-id> [folder]"
    exit 1
fi

ESCAPED_NAME="$(escape_as "$NAME")"

# Auto-detect Notes account UUID for attachment paths
ACCOUNTS_DIR="$HOME/Library/Group Containers/group.com.apple.notes/Accounts/"
ACCOUNT_UUID=""
if [ -d "$ACCOUNTS_DIR" ]; then
    ACCOUNT_UUID=$(ls -1 "$ACCOUNTS_DIR" 2>/dev/null | grep -iE '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$' | head -1)
fi
FALLBACK_BASE=""
PREVIEW_BASE=""
if [ -n "$ACCOUNT_UUID" ]; then
    FALLBACK_BASE="$ACCOUNTS_DIR/$ACCOUNT_UUID/FallbackPDFs"
    PREVIEW_BASE="$ACCOUNTS_DIR/$ACCOUNT_UUID/Previews"
fi

# Get output dir for extracted files
OUTPUT_DIR="/tmp/notes-export/"
mkdir -p "$OUTPUT_DIR"

# Function to process attachment IDs and output paths
process_attachments() {
    local attachment_data="$1"
    local note_title="$2"
    
    if [ -z "$attachment_data" ] || [ "$attachment_data" = "ATTACHMENT_DATA:" ]; then
        return
    fi
    
    # Remove the prefix
    local ids="${attachment_data#ATTACHMENT_DATA:}"
    
    # Convert comma-separated to array
    IFS=',' read -ra ID_ARRAY <<< "$ids"
    
    local extracted_count=0
    for content_id in "${ID_ARRAY[@]}"; do
        # Trim whitespace
        content_id=$(echo "$content_id" | xargs)
        
        local found_file=""
        local file_type=""
        
        # Check for FallbackPDF
        if [ -n "$FALLBACK_BASE" ] && [ -d "$FALLBACK_BASE/$content_id" ]; then
            local pdf_path=$(find "$FALLBACK_BASE/$content_id" -name "FallbackPDF.pdf" 2>/dev/null | head -1)
            if [ -n "$pdf_path" ] && [ -f "$pdf_path" ]; then
                found_file="$pdf_path"
                file_type="pdf"
            fi
        fi
        
        # Check for preview images
        if [ -z "$found_file" ] && [ -n "$PREVIEW_BASE" ] && [ -d "$PREVIEW_BASE" ]; then
            local preview_files=$(find "$PREVIEW_BASE" -name "$content_id-*.png" 2>/dev/null)
            if [ -n "$preview_files" ]; then
                found_file=$(ls -S $preview_files 2>/dev/null | head -1)
                file_type="png"
            fi
        fi
        
        if [ -n "$found_file" ]; then
            # Create output filename
            local safe_title=$(echo "$note_title" | tr -cd '[:alnum:]._-' | cut -c1-50)
            local output_filename="${safe_title}-${extracted_count}.${file_type}"
            local output_path="$OUTPUT_DIR/$output_filename"
            
            # Handle duplicates
            local counter=1
            while [ -f "$output_path" ]; do
                output_filename="${safe_title}-${extracted_count}(${counter}).${file_type}"
                output_path="$OUTPUT_DIR/$output_filename"
                counter=$((counter + 1))
            done
            
            cp "$found_file" "$output_path"
            echo "  → $output_path"
            extracted_count=$((extracted_count + 1))
        fi
    done
}

# ID lookup — direct, ignores folder
if [[ "$NAME" == x-coredata* ]]; then
    RESULT=$(osascript <<EOF
tell application "Notes"
    try
        set n to note id "$ESCAPED_NAME"
        set noteTitle to name of n
        set noteBody to plaintext of n
        set noteDate to modification date of n
        set noteId to id of n
        try
            set noteFolder to name of container of n
        on error
            set noteFolder to "unknown"
        end try
        
        -- Get attachment info
        set attachmentCount to 0
        set attachmentIds to {}
        repeat with a in attachments of n
            try
                set contentId to content identifier of a
                set attachmentIds to attachmentIds & {contentId}
                set attachmentCount to attachmentCount + 1
            end try
        end repeat
        
        set output to "Title: " & noteTitle & linefeed & "Folder: " & noteFolder & linefeed & "Modified: " & (noteDate as text) & linefeed & "ID: " & noteId & linefeed & "---" & linefeed & noteBody
        
        return output & linefeed & "ATTACHMENT_DATA:" & attachmentCount & "|" & (attachmentIds as string) & "|" & noteTitle
    on error errMsg
        return "Error: " & errMsg
    end try
end tell
EOF
)
else
    # Name lookup — search in folder or all notes
    resolve_folder "$FOLDER"
    
    RESULT=$(osascript <<EOF
tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "Error: " & errMsg
    end try
    
    set searchTerm to "$ESCAPED_NAME" as text
    repeat with n in noteList
        if name of n contains searchTerm then
            set noteTitle to name of n
            set noteBody to plaintext of n
            set noteDate to modification date of n
            set noteId to id of n
            try
                set noteFolder to name of container of n
            on error
                set noteFolder to "unknown"
            end try
            
            -- Get attachment info
            set attachmentCount to 0
            set attachmentIds to {}
            repeat with a in attachments of n
                try
                    set contentId to content identifier of a
                    set attachmentIds to attachmentIds & {contentId}
                    set attachmentCount to attachmentCount + 1
                end try
            end repeat
            
            set output to "Title: " & noteTitle & linefeed & "Folder: " & noteFolder & linefeed & "Modified: " & (noteDate as text) & linefeed & "ID: " & noteId & linefeed & "---" & linefeed & noteBody
            
            return output & linefeed & "ATTACHMENT_DATA:" & attachmentCount & "|" & (attachmentIds as string) & "|" & noteTitle
        end if
    end repeat
    return "Error: No note matching '" & searchTerm & "' found"
end tell
EOF
)
fi

# Check for errors
if [[ "$RESULT" == Error:* ]]; then
    echo "$RESULT"
    exit 1
fi

# Split result into content and attachment data
if [[ "$RESULT" == *ATTACHMENT_DATA:* ]]; then
    # Extract attachment section
    ATTACHMENT_SECTION=$(echo "$RESULT" | grep "ATTACHMENT_DATA:")
    # Remove attachment section from main output
    MAIN_OUTPUT=$(echo "$RESULT" | sed '/ATTACHMENT_DATA:/d')
    
    # Parse attachment data: count|ids|title
    ATTACHMENT_DATA="${ATTACHMENT_SECTION#ATTACHMENT_DATA:}"
    IFS='|' read -r ATTACHMENT_COUNT ATTACHMENT_IDS NOTE_TITLE <<< "$ATTACHMENT_DATA"
    
    # Output main content
    echo "$MAIN_OUTPUT"
    
    # Output attachment info if any
    if [ "$ATTACHMENT_COUNT" -gt 0 ] 2>/dev/null; then
        echo ""
        echo "Attachments: $ATTACHMENT_COUNT file"
        if [ "$ATTACHMENT_COUNT" -gt 1 ]; then
            echo -n "s"
            echo ""
        fi
        
        # Process and output paths
        if [ -n "$ATTACHMENT_IDS" ]; then
            process_attachments "ATTACHMENT_DATA:$ATTACHMENT_IDS" "$NOTE_TITLE"
        fi
    fi
else
    # No attachments
    echo "$RESULT"
fi


path: scripts/notes-search.sh
content: #!/bin/bash
# Search notes by title then body content, or via Spotlight
# Usage: notes-search.sh <query> [folder] [limit] [--title-only] [--spotlight]
# Folder supports / paths. Use "" for all folders.
# --spotlight uses mdfind to search inside scanned documents (OCR text)

source "$(dirname "$0")/_resolve_folder.sh"

QUERY="" FOLDER="" LIMIT="10" TITLE_ONLY=false SPOTLIGHT=false

for arg in "$@"; do
    case "$arg" in
        --title-only) TITLE_ONLY=true ;;
        --spotlight) SPOTLIGHT=true ;;
        *) 
            if [ -z "$QUERY" ]; then QUERY="$arg"
            elif [ -z "$FOLDER" ]; then FOLDER="$arg"
            else LIMIT="$arg"
            fi ;;
    esac
done

if [ -z "$QUERY" ]; then
    echo "Usage: notes-search.sh <query> [folder] [limit] [--title-only] [--spotlight]"
    exit 1
fi

# Handle spotlight search
if [ "$SPOTLIGHT" = "true" ]; then
    # Search in Notes domain using mdfind (OCR-enabled search)
    if [ -n "$FOLDER" ]; then
        echo "⚠ Spotlight search ignores folder filter (searching all notes)" >&2
    fi
    
    echo "Searching via Spotlight (OCR-enabled)..." >&2
    
    # Use mdfind to search Notes metadata
    # kMDItemContentType = com.apple.notes.note
    results=$(mdfind "kMDItemTextContent == '*$QUERY*' && kMDItemContentType == 'com.apple.notes.note'" 2>/dev/null | head -$LIMIT)
    
    if [ -z "$results" ]; then
        echo "No notes matching '$QUERY' found via Spotlight"
        exit 0
    fi
    
    # Parse results and get note info via AppleScript
    for notePath in $results; do
        # Extract note ID from path or use AppleScript to find it
        noteName=$(basename "$notePath" .note)
        
        # Get note details from Notes app
        osascript <<EOF
tell application "Notes"
    try
        repeat with n in every note
            if name of n contains "$QUERY" then
                set noteId to id of n
                set noteDate to modification date of n
                try
                    set noteFolder to name of container of n
                on error
                    set noteFolder to "?"
                end try
                set y to year of noteDate
                set m to (month of noteDate as integer)
                set d to day of noteDate
                set dateStr to (y as text) & "-" & text -2 thru -1 of ("0" & m) & "-" & text -2 thru -1 of ("0" & d)
                return noteId & " | " & dateStr & " | " & noteFolder & " | " & (name of n)
            end if
        end repeat
    end try
end tell
EOF
    done
    exit 0
fi

# Regular AppleScript search
ESCAPED_QUERY="$(escape_as "$QUERY")"

resolve_folder "$FOLDER"

if [ -z "$FOLDER" ]; then
    echo "⚠ Searching all folders — this may be slow on large collections. Specify a folder for speed." >&2
fi

if [ "$TITLE_ONLY" = "true" ]; then
    BODY_SEARCH="false"
else
    BODY_SEARCH="true"
fi

osascript <<EOF
tell application "Notes"
    try
        $FOLDER_SCRIPT
    on error errMsg
        return "Error: " & errMsg
    end try
    
    set searchTerm to "$ESCAPED_QUERY" as text
    set maxCount to $LIMIT as integer
    set doBodySearch to $BODY_SEARCH as boolean
    set output to ""
    set titleCount to 0
    
    -- Title search (fast)
    repeat with n in noteList
        if titleCount ≥ maxCount then exit repeat
        if name of n contains searchTerm then
            set noteId to id of n
            set noteDate to modification date of n
            try
                set noteFolder to name of container of n
            on error
                set noteFolder to "?"
            end try
            set y to year of noteDate
            set m to (month of noteDate as integer)
            set d to day of noteDate
            set dateStr to (y as text) & "-" & text -2 thru -1 of ("0" & m) & "-" & text -2 thru -1 of ("0" & d)
            set output to output & noteId & " | " & dateStr & " | " & noteFolder & " | " & (name of n) & linefeed
            set titleCount to titleCount + 1
        end if
    end repeat
    
    -- Body search fallback (slower)
    if output is "" and doBodySearch then
        set i to 0
        repeat with n in noteList
            if i ≥ maxCount then exit repeat
            try
                set noteBody to plaintext of n
                if noteBody contains searchTerm then
                    set noteId to id of n
                    set noteDate to modification date of n
                    try
                        set noteFolder to name of container of n
                    on error
                        set noteFolder to "?"
                    end try
                    set y to year of noteDate
                    set m to (month of noteDate as integer)
                    set d to day of noteDate
                    set dateStr to (y as text) & "-" & text -2 thru -1 of ("0" & m) & "-" & text -2 thru -1 of ("0" & d)
                    set output to output & noteId & " | " & dateStr & " | " & noteFolder & " | " & (name of n) & linefeed
                    set i to i + 1
                end if
            end try
        end repeat
    end if
    
    if output is "" then return "No notes matching '" & searchTerm & "' found"
    return output
end tell
EOF


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (CRITICAL)