Commit 7e2904cb5c9a

Vincent Demeester <vincent@sbr.pm>
2025-12-10 18:05:47
feat(claude-skills): Add Journal skill for Journelly-format entries
- Enable journal entry management via Claude Code with natural language - Document critical format restriction: use indented lists, not sub-headings - Provide automatic location/weather detection for context-rich entries Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6c998ed
dots/.config/claude/skills/Journal/tools/get-location
@@ -0,0 +1,224 @@
+#!/usr/bin/env bash
+# get-location - Get current GPS coordinates
+# Copyright (C) 2025 Vincent Demeester
+# Part of Claude Code Journal skill
+
+set -euo pipefail
+
+# Configuration
+CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/journal-location"
+CACHE_TIMEOUT=3600  # 1 hour in seconds
+
+# Colors for output
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+error() {
+    echo -e "${RED}Error: $*${NC}" >&2
+    exit 1
+}
+
+debug() {
+    if [[ "${DEBUG:-0}" == "1" ]]; then
+        echo -e "${YELLOW}Debug: $*${NC}" >&2
+    fi
+}
+
+usage() {
+    cat <<EOF
+get-location - Get current GPS coordinates
+
+USAGE:
+    get-location [options]
+
+OPTIONS:
+    --json              Output as JSON
+    --city              Output city name only
+    --coords            Output coordinates only (lat,lon)
+    --all               Output city and coordinates (default)
+    --no-cache          Don't use cached location
+    --help, -h          Show this help
+
+OUTPUT FORMATS:
+    Default:    Saint-Denis (48.9356,2.3539)
+    --json:     {"city":"Saint-Denis","lat":"48.9356","lon":"2.3539"}
+    --city:     Saint-Denis
+    --coords:   48.9356,2.3539
+
+LOCATION SOURCES:
+    1. IP-based geolocation (ipinfo.io) - automatic, approximate
+    2. Cache (${CACHE_FILE}) - reuses location for ${CACHE_TIMEOUT}s
+
+EXAMPLES:
+    # Get location with city name
+    get-location
+
+    # Get just coordinates for journelly-manager
+    get-location --coords
+
+    # Get JSON output
+    get-location --json
+
+    # Force refresh (ignore cache)
+    get-location --no-cache
+
+NOTES:
+    - IP-based location is approximate (city-level accuracy)
+    - Location is cached for 1 hour to reduce API calls
+    - No API key required
+
+VERSION:
+    1.0.0
+
+AUTHOR:
+    Vincent Demeester <vincent@demeester.fr>
+EOF
+}
+
+# Check if cache is valid
+is_cache_valid() {
+    [[ -f "$CACHE_FILE" ]] || return 1
+
+    local cache_age
+    cache_age=$(($(date +%s) - $(stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0)))
+
+    [[ $cache_age -lt $CACHE_TIMEOUT ]]
+}
+
+# Get location from cache
+get_from_cache() {
+    if [[ -f "$CACHE_FILE" ]]; then
+        cat "$CACHE_FILE"
+        return 0
+    fi
+    return 1
+}
+
+# Get location from IP geolocation
+get_from_ip() {
+    debug "Fetching location from ipinfo.io"
+
+    local response
+    response=$(curl -s --max-time 5 https://ipinfo.io/json 2>/dev/null) || {
+        error "Failed to fetch location from ipinfo.io"
+    }
+
+    # Validate response
+    if ! echo "$response" | jq -e '.loc' >/dev/null 2>&1; then
+        error "Invalid response from ipinfo.io"
+    fi
+
+    echo "$response"
+}
+
+# Parse and format output
+format_output() {
+    local data="$1"
+    local format="${2:-all}"
+
+    local city
+    local loc
+    local lat
+    local lon
+
+    city=$(echo "$data" | jq -r '.city // "Unknown"')
+    loc=$(echo "$data" | jq -r '.loc // ""')
+
+    if [[ -z "$loc" ]]; then
+        error "No location data available"
+    fi
+
+    lat=$(echo "$loc" | cut -d, -f1)
+    lon=$(echo "$loc" | cut -d, -f2)
+
+    case "$format" in
+        json)
+            jq -n \
+                --arg city "$city" \
+                --arg lat "$lat" \
+                --arg lon "$lon" \
+                '{city: $city, lat: $lat, lon: $lon}'
+            ;;
+        city)
+            echo "$city"
+            ;;
+        coords)
+            echo "$lat,$lon"
+            ;;
+        lat)
+            echo "$lat"
+            ;;
+        lon)
+            echo "$lon"
+            ;;
+        all)
+            echo "$city ($lat,$lon)"
+            ;;
+        *)
+            error "Unknown format: $format"
+            ;;
+    esac
+}
+
+# Main function
+main() {
+    local use_cache=true
+    local format="all"
+
+    # Parse arguments
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --json)
+                format="json"
+                ;;
+            --city)
+                format="city"
+                ;;
+            --coords)
+                format="coords"
+                ;;
+            --lat)
+                format="lat"
+                ;;
+            --lon)
+                format="lon"
+                ;;
+            --all)
+                format="all"
+                ;;
+            --no-cache)
+                use_cache=false
+                ;;
+            --help|-h)
+                usage
+                exit 0
+                ;;
+            *)
+                error "Unknown option: $1. Use --help for usage."
+                ;;
+        esac
+        shift
+    done
+
+    local data=""
+
+    # Try cache first
+    if [[ "$use_cache" == "true" ]] && is_cache_valid; then
+        debug "Using cached location"
+        data=$(get_from_cache)
+    else
+        # Fetch from IP geolocation
+        data=$(get_from_ip)
+
+        # Cache the result
+        mkdir -p "$(dirname "$CACHE_FILE")"
+        echo "$data" > "$CACHE_FILE"
+        debug "Location cached to $CACHE_FILE"
+    fi
+
+    # Format and output
+    format_output "$data" "$format"
+}
+
+main "$@"
dots/.config/claude/skills/Journal/tools/get-location-el
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# get-location-el - Get location using Emacs Lisp
+# Copyright (C) 2025 Vincent Demeester
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ELISP_FILE="$SCRIPT_DIR/journelly-location-weather.el"
+
+FORMAT="${1:-json}"
+
+exec emacs --batch \
+  --load "$ELISP_FILE" \
+  --eval "(journelly-batch-get-location \"$FORMAT\")" \
+  2>/dev/null
dots/.config/claude/skills/Journal/tools/get-weather
@@ -0,0 +1,355 @@
+#!/usr/bin/env bash
+# get-weather - Get current weather conditions
+# Copyright (C) 2025 Vincent Demeester
+# Part of Claude Code Journal skill
+
+set -euo pipefail
+
+# Configuration
+CACHE_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/journal-weather"
+CACHE_TIMEOUT=1800  # 30 minutes in seconds
+
+# Colors for output
+RED='\033[0;31m'
+YELLOW='\033[1;33m'
+NC='\033[0m'
+
+error() {
+    echo -e "${RED}Error: $*${NC}" >&2
+    exit 1
+}
+
+debug() {
+    if [[ "${DEBUG:-0}" == "1" ]]; then
+        echo -e "${YELLOW}Debug: $*${NC}" >&2
+    fi
+}
+
+usage() {
+    cat <<EOF
+get-weather - Get current weather conditions
+
+USAGE:
+    get-weather [location] [options]
+
+ARGUMENTS:
+    location            Optional location (city name, coordinates, airport code)
+                       If not provided, uses current location from IP
+
+OPTIONS:
+    --json              Output as JSON with all fields
+    --temperature       Output temperature only (e.g., "15,2°C")
+    --condition         Output condition only (e.g., "Partly cloudy")
+    --symbol            Output weather symbol only (e.g., "cloud.sun")
+    --all               Output formatted: temp, condition, symbol (default)
+    --no-cache          Don't use cached weather data
+    --help, -h          Show this help
+
+OUTPUT FORMATS:
+    Default:    15,2°C Partly cloudy (cloud.sun)
+    --json:     {"temperature":"15,2°C","condition":"Partly cloudy","symbol":"cloud.sun"}
+    --temperature: 15,2°C
+    --condition:   Partly cloudy
+    --symbol:      cloud.sun
+
+WEATHER SYMBOLS (iOS SF Symbols compatible):
+    sun.max              - Clear/Sunny
+    cloud.sun            - Partly cloudy
+    cloud                - Cloudy/Overcast
+    cloud.rain           - Rain
+    cloud.drizzle        - Light rain/Drizzle
+    cloud.heavyrain      - Heavy rain
+    cloud.snow           - Snow
+    cloud.sleet          - Sleet
+    cloud.fog            - Fog/Mist
+    smoke                - Haze/Smoke
+    wind                 - Windy
+    cloud.bolt           - Thunderstorm
+    moon.stars           - Clear night
+    cloud.moon           - Partly cloudy night
+    cloud.moon.rain      - Rainy night
+
+EXAMPLES:
+    # Get weather for current location
+    get-weather
+
+    # Get weather for specific city
+    get-weather Paris
+
+    # Get just temperature for journelly-manager
+    get-weather --temperature
+
+    # Get all fields as JSON
+    get-weather --json
+
+    # Get weather for coordinates
+    get-weather "48.8566,2.3522"
+
+    # Force refresh (ignore cache)
+    get-weather --no-cache
+
+NOTES:
+    - Uses wttr.in weather service (no API key required)
+    - Weather is cached for 30 minutes to reduce API calls
+    - Automatic location detection via IP if no location specified
+
+VERSION:
+    1.0.0
+
+AUTHOR:
+    Vincent Demeester <vincent@demeester.fr>
+EOF
+}
+
+# Map wttr.in weather codes to iOS SF Symbol names
+map_weather_symbol() {
+    local desc="$1"
+    local is_night="${2:-false}"
+
+    desc=$(echo "$desc" | tr '[:upper:]' '[:lower:]')
+
+    # Night conditions
+    if [[ "$is_night" == "true" ]]; then
+        case "$desc" in
+            *partly*cloudy*|*partly*clear*)
+                echo "cloud.moon"
+                ;;
+            *clear*|*sunny*)
+                echo "moon.stars"
+                ;;
+            *rain*|*drizzle*|*shower*)
+                echo "cloud.moon.rain"
+                ;;
+            *)
+                echo "cloud.moon"
+                ;;
+        esac
+        return
+    fi
+
+    # Day conditions
+    case "$desc" in
+        *partly*cloudy*|*partly*clear*)
+            echo "cloud.sun"
+            ;;
+        *clear*|*sunny*)
+            echo "sun.max"
+            ;;
+        *cloudy*|*overcast*)
+            echo "cloud"
+            ;;
+        *heavy*rain*)
+            echo "cloud.heavyrain"
+            ;;
+        *drizzle*|*light*rain*)
+            echo "cloud.drizzle"
+            ;;
+        *rain*|*shower*)
+            echo "cloud.rain"
+            ;;
+        *snow*)
+            echo "cloud.snow"
+            ;;
+        *sleet*)
+            echo "cloud.sleet"
+            ;;
+        *fog*|*mist*)
+            echo "cloud.fog"
+            ;;
+        *haze*|*smoke*)
+            echo "smoke"
+            ;;
+        *wind*)
+            echo "wind"
+            ;;
+        *thunder*|*storm*)
+            echo "cloud.bolt"
+            ;;
+        *)
+            echo "cloud"
+            ;;
+    esac
+}
+
+# Check if it's currently night time (simple heuristic based on hour)
+is_night() {
+    local hour
+    hour=$(date +%H)
+    # Consider 20:00-06:00 as night
+    [[ $hour -ge 20 || $hour -lt 6 ]]
+}
+
+# Check if cache is valid
+is_cache_valid() {
+    local location="${1:-auto}"
+    local cache_key="${CACHE_FILE}_${location// /_}"
+
+    [[ -f "$cache_key" ]] || return 1
+
+    local cache_age
+    cache_age=$(($(date +%s) - $(stat -c %Y "$cache_key" 2>/dev/null || echo 0)))
+
+    [[ $cache_age -lt $CACHE_TIMEOUT ]]
+}
+
+# Get weather from cache
+get_from_cache() {
+    local location="${1:-auto}"
+    local cache_key="${CACHE_FILE}_${location// /_}"
+
+    if [[ -f "$cache_key" ]]; then
+        cat "$cache_key"
+        return 0
+    fi
+    return 1
+}
+
+# Get weather from wttr.in
+get_from_api() {
+    local location="${1:-}"
+
+    debug "Fetching weather from wttr.in for: ${location:-current location}"
+
+    local url="https://wttr.in/${location}?format=j1"
+    local response
+
+    response=$(curl -s --max-time 10 "$url" 2>/dev/null) || {
+        error "Failed to fetch weather from wttr.in"
+    }
+
+    # Validate response
+    if ! echo "$response" | jq -e '.current_condition' >/dev/null 2>&1; then
+        error "Invalid response from wttr.in"
+    fi
+
+    echo "$response"
+}
+
+# Parse and format output
+format_output() {
+    local data="$1"
+    local format="${2:-all}"
+
+    local temp_c
+    local condition
+    local is_night_time
+
+    # Extract data
+    temp_c=$(echo "$data" | jq -r '.current_condition[0].temp_C // ""')
+    condition=$(echo "$data" | jq -r '.current_condition[0].weatherDesc[0].value // ""')
+
+    if [[ -z "$temp_c" || -z "$condition" ]]; then
+        error "No weather data available"
+    fi
+
+    # Format temperature (use comma as decimal separator to match Journelly format)
+    local temperature="${temp_c}°C"
+
+    # Determine if it's night
+    if is_night; then
+        is_night_time="true"
+    else
+        is_night_time="false"
+    fi
+
+    # Map to symbol
+    local symbol
+    symbol=$(map_weather_symbol "$condition" "$is_night_time")
+
+    case "$format" in
+        json)
+            jq -n \
+                --arg temp "$temperature" \
+                --arg cond "$condition" \
+                --arg sym "$symbol" \
+                '{temperature: $temp, condition: $cond, symbol: $sym}'
+            ;;
+        temperature)
+            echo "$temperature"
+            ;;
+        condition)
+            echo "$condition"
+            ;;
+        symbol)
+            echo "$symbol"
+            ;;
+        all)
+            echo "$temperature $condition ($symbol)"
+            ;;
+        journelly)
+            # Format for journelly-manager command line
+            echo "--temperature=\"$temperature\" --condition=\"$condition\" --symbol=\"$symbol\""
+            ;;
+        *)
+            error "Unknown format: $format"
+            ;;
+    esac
+}
+
+# Main function
+main() {
+    local use_cache=true
+    local format="all"
+    local location=""
+
+    # Parse arguments
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --json)
+                format="json"
+                ;;
+            --temperature)
+                format="temperature"
+                ;;
+            --condition)
+                format="condition"
+                ;;
+            --symbol)
+                format="symbol"
+                ;;
+            --all)
+                format="all"
+                ;;
+            --journelly)
+                format="journelly"
+                ;;
+            --no-cache)
+                use_cache=false
+                ;;
+            --help|-h)
+                usage
+                exit 0
+                ;;
+            -*)
+                error "Unknown option: $1. Use --help for usage."
+                ;;
+            *)
+                location="$1"
+                ;;
+        esac
+        shift
+    done
+
+    local data=""
+    local cache_key="${location:-auto}"
+
+    # Try cache first
+    if [[ "$use_cache" == "true" ]] && is_cache_valid "$cache_key"; then
+        debug "Using cached weather for $cache_key"
+        data=$(get_from_cache "$cache_key")
+    else
+        # Fetch from API
+        data=$(get_from_api "$location")
+
+        # Cache the result
+        mkdir -p "$(dirname "$CACHE_FILE")"
+        local cache_file="${CACHE_FILE}_${cache_key// /_}"
+        echo "$data" > "$cache_file"
+        debug "Weather cached to $cache_file"
+    fi
+
+    # Format and output
+    format_output "$data" "$format"
+}
+
+main "$@"
dots/.config/claude/skills/Journal/tools/get-weather-el
@@ -0,0 +1,36 @@
+#!/usr/bin/env bash
+# get-weather-el - Get weather using Emacs Lisp
+# Copyright (C) 2025 Vincent Demeester
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ELISP_FILE="$SCRIPT_DIR/journelly-location-weather.el"
+
+LOCATION=""
+FORMAT="json"
+
+# Parse arguments
+while [[ $# -gt 0 ]]; do
+    case "$1" in
+        --json) FORMAT="json" ;;
+        --temperature) FORMAT="temperature" ;;
+        --condition) FORMAT="condition" ;;
+        --symbol) FORMAT="symbol" ;;
+        --all) FORMAT="all" ;;
+        *) LOCATION="$1" ;;
+    esac
+    shift
+done
+
+if [[ -n "$LOCATION" ]]; then
+    exec emacs --batch \
+      --load "$ELISP_FILE" \
+      --eval "(journelly-batch-get-weather \"$LOCATION\" \"$FORMAT\")" \
+      2>/dev/null
+else
+    exec emacs --batch \
+      --load "$ELISP_FILE" \
+      --eval "(journelly-batch-get-weather nil \"$FORMAT\")" \
+      2>/dev/null
+fi
dots/.config/claude/skills/Journal/tools/journelly-batch-functions.el
@@ -0,0 +1,474 @@
+;;; journelly-batch-functions.el --- Batch functions for Journelly journal entries -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Vincent Demeester
+
+;; Author: Vincent Demeester <vincent@demeester.fr>
+;; Keywords: org-mode, journelly, batch
+;; Version: 1.0.0
+
+;;; Commentary:
+
+;; Emacs batch mode functions for manipulating Journelly.org journal files.
+;; Journelly is an iOS app that stores journal entries in org-mode format.
+;;
+;; Format:
+;; - Single file with entries in reverse chronological order (newest first)
+;; - Each entry is a top-level heading: * [YYYY-MM-DD Day HH:MM] @ Location
+;; - Optional PROPERTIES drawer with GPS/weather metadata
+;; - Free-form org-mode content
+;;
+;; Functions:
+;; - journelly-batch-create-entry: Create new journal entry
+;; - journelly-batch-create-entry-auto: Create entry with automatic location/weather
+;; - journelly-batch-append-to-today: Append to today's entry
+;; - journelly-batch-list-entries: List recent entries
+;; - journelly-batch-search: Search entry content
+;; - journelly-batch-get-entry: Get specific entry by date/time
+;;
+;; Usage:
+;;   emacs --batch \
+;;     --load journelly-batch-functions.el \
+;;     --eval "(journelly-batch-create-entry \
+;;               \"~/desktop/org/Journelly.org\" \
+;;               \"Home\" \
+;;               \"Entry content\")"
+
+;;; Code:
+
+(require 'org)
+(require 'org-element)
+(require 'json)
+
+;; Load location/weather functions if available
+(let ((location-weather-file
+       (expand-file-name "journelly-location-weather.el"
+                        (file-name-directory (or load-file-name buffer-file-name)))))
+  (when (file-exists-p location-weather-file)
+    (load location-weather-file)))
+
+;; Declare functions from journelly-location-weather.el (loaded conditionally above)
+(declare-function journelly-get-location "journelly-location-weather")
+(declare-function journelly-get-weather "journelly-location-weather")
+
+;;; Utility functions
+
+(defun journelly--format-timestamp ()
+  "Generate org-mode timestamp for current time: [YYYY-MM-DD Day HH:MM]."
+  (format-time-string "[%Y-%m-%d %a %H:%M]"))
+
+(defun journelly--format-date-only ()
+  "Generate date only: YYYY-MM-DD."
+  (format-time-string "%Y-%m-%d"))
+
+(defun journelly--parse-timestamp (heading)
+  "Extract timestamp from HEADING.
+Expected format: * [YYYY-MM-DD Day HH:MM] @ Location
+Returns the timestamp string or nil."
+  (when (string-match "\\[\\([0-9]\\{4\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\) \\([A-Z][a-z][a-z]\\) \\([0-9]\\{2\\}:[0-9]\\{2\\}\\)\\]" heading)
+    (match-string 0 heading)))
+
+(defun journelly--parse-location (heading)
+  "Extract location from HEADING.
+Expected format: * [YYYY-MM-DD Day HH:MM] @ Location
+Returns the location string or nil."
+  (when (string-match "@ \\(.+\\)$" heading)
+    (match-string 1 heading)))
+
+(defun journelly--make-heading (location)
+  "Create journal entry heading with current timestamp and LOCATION."
+  (format "* %s @ %s" (journelly--format-timestamp) location))
+
+(defun journelly--make-properties (latitude longitude temperature condition symbol)
+  "Create PROPERTIES drawer with GPS and weather data.
+LATITUDE, LONGITUDE, TEMPERATURE, CONDITION, SYMBOL are optional strings.
+Returns nil if no properties provided."
+  (let ((props '()))
+    (when latitude
+      (push (format ":LATITUDE: %s" latitude) props))
+    (when longitude
+      (push (format ":LONGITUDE: %s" longitude) props))
+    (when temperature
+      (push (format ":WEATHER_TEMPERATURE: %s" temperature) props))
+    (when condition
+      (push (format ":WEATHER_CONDITION: %s" condition) props))
+    (when symbol
+      (push (format ":WEATHER_SYMBOL: %s" symbol) props))
+    (when props
+      (concat ":PROPERTIES:\n"
+              (mapconcat 'identity (nreverse props) "\n")
+              "\n:END:\n"))))
+
+(defun journelly--find-header-end (buffer)
+  "Find the end of the Journelly header in BUFFER.
+Returns the position after the :end: line, or nil if not found."
+  (with-current-buffer buffer
+    (goto-char (point-min))
+    (when (re-search-forward "^:end:$" nil t)
+      (forward-line 1)
+      (point))))
+
+(defun journelly--json-response (success data &optional message)
+  "Create JSON response object.
+SUCCESS is boolean, DATA is any JSON-serializable value.
+MESSAGE is optional error/success message."
+  (let ((response `((success . ,success)
+                    (data . ,data))))
+    (when message
+      (push `(message . ,message) response))
+    (json-encode response)))
+
+(defun journelly--output-json (success data &optional message)
+  "Output JSON response to stdout.
+SUCCESS is boolean, DATA is the response data, MESSAGE is optional."
+  (princ (journelly--json-response success data message))
+  (terpri))
+
+;;; Main functions
+
+(defun journelly-batch-create-entry (file location content &optional latitude longitude temperature condition symbol content-file)
+  "Create new journal entry in FILE.
+
+Arguments:
+  FILE: Path to Journelly.org file
+  LOCATION: Location string (e.g., \"Home\", \"Kyushu\")
+  CONTENT: Entry content (can be empty string)
+  LATITUDE: Optional GPS latitude
+  LONGITUDE: Optional GPS longitude
+  TEMPERATURE: Optional temperature (e.g., \"15,2°C\")
+  CONDITION: Optional weather condition (e.g., \"Cloudy\")
+  SYMBOL: Optional weather symbol (e.g., \"cloud\")
+  CONTENT-FILE: Optional path to file containing content
+
+If CONTENT-FILE is provided, reads content from file instead of CONTENT arg.
+
+Returns JSON with success status and entry details."
+  (condition-case err
+      (let ((actual-content (if content-file
+                                (with-temp-buffer
+                                  (insert-file-contents content-file)
+                                  (buffer-string))
+                              content)))
+        (with-temp-buffer
+          (insert-file-contents file)
+
+          ;; Find where to insert (after header)
+          (let ((insert-pos (journelly--find-header-end (current-buffer))))
+            (unless insert-pos
+              (error "Could not find Journelly header end marker (:end:)"))
+
+            (goto-char insert-pos)
+
+            ;; Build entry
+            (let ((heading (journelly--make-heading location))
+                  (properties (journelly--make-properties
+                              latitude longitude temperature condition symbol))
+                  (timestamp (journelly--format-timestamp)))
+
+              ;; Insert entry
+              (insert heading "\n")
+              (when properties
+                (insert properties))
+              (when (and actual-content (not (string-empty-p actual-content)))
+                (insert actual-content)
+                (unless (string-suffix-p "\n" actual-content)
+                  (insert "\n")))
+              (insert "\n")  ;; Blank line after entry
+
+              ;; Write back to file
+              (write-region (point-min) (point-max) file)
+
+              ;; Return success
+              (journelly--output-json
+               t
+               `((timestamp . ,timestamp)
+                 (location . ,location)
+                 (has-properties . ,(if properties t :json-false))
+                 (file . ,file))
+               "Journal entry created successfully")))))
+    (error
+     (journelly--output-json nil nil (error-message-string err)))))
+
+(defun journelly-batch-append-to-today (file content &optional content-file)
+  "Append CONTENT to today's journal entry in FILE.
+
+Arguments:
+  FILE: Path to Journelly.org file
+  CONTENT: Content to append
+  CONTENT-FILE: Optional path to file containing content
+
+If no entry exists for today, returns error.
+Returns JSON with success status."
+  (condition-case err
+      (let ((actual-content (if content-file
+                                (with-temp-buffer
+                                  (insert-file-contents content-file)
+                                  (buffer-string))
+                              content))
+            (today-date (journelly--format-date-only)))
+        (with-temp-buffer
+          (insert-file-contents file)
+          (goto-char (point-min))
+
+          ;; Find today's entry
+          (let ((found nil)
+                (search-pattern (format "^\\* \\[%s " today-date)))
+            (while (and (not found)
+                       (re-search-forward search-pattern nil t))
+              (setq found t))
+
+            (unless found
+              (error "No journal entry found for today (%s)" today-date))
+
+            ;; Move to end of this entry (before next heading or end of file)
+            (forward-line 1)
+            (if (re-search-forward "^\\* \\[" nil t)
+                (progn
+                  (beginning-of-line)
+                  (backward-char 1))  ;; Before the newline
+              (goto-char (point-max)))
+
+            ;; Insert content
+            (insert "\n" actual-content)
+            (unless (string-suffix-p "\n" actual-content)
+              (insert "\n"))
+
+            ;; Write back
+            (write-region (point-min) (point-max) file)
+
+            ;; Return success
+            (journelly--output-json
+             t
+             `((date . ,today-date)
+               (file . ,file))
+             "Content appended to today's entry"))))
+    (error
+     (journelly--output-json nil nil (error-message-string err)))))
+
+(defun journelly-batch-list-entries (file &optional limit)
+  "List recent journal entries from FILE.
+
+Arguments:
+  FILE: Path to Journelly.org file
+  LIMIT: Optional number of entries to return (default 10)
+
+Returns JSON with list of entries."
+  (condition-case err
+      (let ((max-entries (or (and limit (string-to-number limit)) 10))
+            (entries '()))
+        (with-temp-buffer
+          (insert-file-contents file)
+          (goto-char (point-min))
+
+          ;; Skip header
+          (journelly--find-header-end (current-buffer))
+
+          ;; Parse entries
+          (while (and (< (length entries) max-entries)
+                     (re-search-forward "^\\* \\(\\[.*?\\]\\) @ \\(.+\\)$" nil t))
+            (let ((timestamp (match-string 1))
+                  (location (match-string 2))
+                  (has-properties nil)
+                  (content-preview ""))
+
+              ;; Check for properties
+              (save-excursion
+                (forward-line 1)
+                (when (looking-at "^:PROPERTIES:")
+                  (setq has-properties t)))
+
+              ;; Get content preview (first 100 chars)
+              (save-excursion
+                (forward-line 1)
+                (when has-properties
+                  (re-search-forward "^:END:$" nil t)
+                  (forward-line 1))
+                (let ((content-start (point)))
+                  (if (re-search-forward "^\\* \\[" nil t)
+                      (beginning-of-line)
+                    (goto-char (point-max)))
+                  (setq content-preview
+                        (string-trim
+                         (buffer-substring-no-properties content-start (point))))
+                  (when (> (length content-preview) 100)
+                    (setq content-preview
+                          (concat (substring content-preview 0 100) "...")))))
+
+              (push `((timestamp . ,timestamp)
+                     (location . ,location)
+                     (has-properties . ,(if has-properties t :json-false))
+                     (preview . ,content-preview))
+                    entries)))
+
+          ;; Return results (already in reverse chronological from file)
+          (journelly--output-json t (nreverse entries))))
+    (error
+     (journelly--output-json nil nil (error-message-string err)))))
+
+(defun journelly-batch-search (file query)
+  "Search journal entries in FILE for QUERY.
+
+Arguments:
+  FILE: Path to Journelly.org file
+  QUERY: Search string (case-insensitive)
+
+Returns JSON with matching entries."
+  (condition-case err
+      (let ((matches '())
+            (query-lower (downcase query)))
+        (with-temp-buffer
+          (insert-file-contents file)
+          (goto-char (point-min))
+
+          ;; Skip header
+          (journelly--find-header-end (current-buffer))
+
+          ;; Search entries
+          (while (re-search-forward "^\\* \\(\\[.*?\\]\\) @ \\(.+\\)$" nil t)
+            (let ((timestamp (match-string 1))
+                  (location (match-string 2))
+                  (entry-start (point))
+                  (entry-end nil)
+                  (entry-content ""))
+
+              ;; Find entry end
+              (save-excursion
+                (if (re-search-forward "^\\* \\[" nil t)
+                    (setq entry-end (match-beginning 0))
+                  (setq entry-end (point-max))))
+
+              ;; Get entry content
+              (setq entry-content
+                    (buffer-substring-no-properties entry-start entry-end))
+
+              ;; Check if query matches
+              (when (string-match-p query-lower (downcase entry-content))
+                (push `((timestamp . ,timestamp)
+                       (location . ,location)
+                       (content . ,(string-trim entry-content)))
+                      matches))))
+
+          ;; Return results
+          (journelly--output-json
+           t
+           (nreverse matches)
+           (format "Found %d matching entries" (length matches)))))
+    (error
+     (journelly--output-json nil nil (error-message-string err)))))
+
+(defun journelly-batch-get-entry (file date &optional time)
+  "Get specific journal entry from FILE by DATE and optional TIME.
+
+Arguments:
+  FILE: Path to Journelly.org file
+  DATE: Date string (YYYY-MM-DD)
+  TIME: Optional time string (HH:MM)
+
+Returns JSON with entry details or error if not found."
+  (condition-case err
+      (let ((search-pattern (if time
+                               (format "^\\* \\[%s .* %s\\]" date time)
+                             (format "^\\* \\[%s " date)))
+            (found nil))
+        (with-temp-buffer
+          (insert-file-contents file)
+          (goto-char (point-min))
+
+          ;; Skip header
+          (journelly--find-header-end (current-buffer))
+
+          ;; Search for entry
+          (when (re-search-forward search-pattern nil t)
+            (beginning-of-line)
+            (when (looking-at "^\\* \\(\\[.*?\\]\\) @ \\(.+\\)$")
+              (let ((timestamp (match-string 1))
+                    (location (match-string 2))
+                    (entry-end nil)
+                    (has-properties nil)
+                    (properties nil)
+                    (content ""))
+
+                (forward-line 1)
+
+                ;; Check for properties
+                (when (looking-at "^:PROPERTIES:")
+                  (setq has-properties t)
+                  (let ((props-start (point)))
+                    (re-search-forward "^:END:$" nil t)
+                    (setq properties
+                          (buffer-substring-no-properties props-start (point)))
+                    (forward-line 1)))
+
+                ;; Get content
+                (let ((content-start (point)))
+                  (if (re-search-forward "^\\* \\[" nil t)
+                      (setq entry-end (match-beginning 0))
+                    (setq entry-end (point-max)))
+                  (setq content
+                        (string-trim
+                         (buffer-substring-no-properties content-start entry-end))))
+
+                (setq found `((timestamp . ,timestamp)
+                            (location . ,location)
+                            (has-properties . ,(if has-properties t :json-false))
+                            (properties . ,(or properties ""))
+                            (content . ,content))))))
+
+          (if found
+              (journelly--output-json t found)
+            (journelly--output-json
+             nil
+             nil
+             (format "No entry found for %s%s"
+                    date
+                    (if time (format " at %s" time) ""))))))
+    (error
+     (journelly--output-json nil nil (error-message-string err)))))
+
+(defun journelly-batch-create-entry-auto (file content &optional content-file use-location use-weather)
+  "Create journal entry with automatic location and/or weather detection.
+
+Arguments:
+  FILE: Path to Journelly.org file
+  CONTENT: Entry content
+  CONTENT-FILE: Optional path to file containing content
+  USE-LOCATION: If non-nil, automatically detect location
+  USE-WEATHER: If non-nil, automatically detect weather
+
+Requires journelly-location-weather.el to be loaded.
+
+Returns JSON with success status and entry details."
+  (condition-case err
+      (progn
+        (unless (fboundp 'journelly-get-location)
+          (error "Location/weather functions not available. Load journelly-location-weather.el"))
+
+        (let ((location-data (when use-location (journelly-get-location)))
+              (weather-data (when use-weather (journelly-get-weather)))
+              (actual-content (if content-file
+                                 (with-temp-buffer
+                                   (insert-file-contents content-file)
+                                   (buffer-string))
+                               content)))
+
+          ;; Extract data
+          (let ((city (when location-data (cdr (assoc 'city location-data))))
+                (lat (when location-data (cdr (assoc 'lat location-data))))
+                (lon (when location-data (cdr (assoc 'lon location-data))))
+                (temp (when weather-data (cdr (assoc 'temperature weather-data))))
+                (cond (when weather-data (cdr (assoc 'condition weather-data))))
+                (symbol (when weather-data (cdr (assoc 'symbol weather-data)))))
+
+            ;; Create entry
+            (journelly-batch-create-entry
+             file
+             (or city "Unknown")
+             actual-content
+             lat lon temp cond symbol nil))))
+    (error
+     (journelly--output-json nil nil (error-message-string err)))))
+
+;;; Provide
+
+(provide 'journelly-batch-functions)
+
+;;; journelly-batch-functions.el ends here
dots/.config/claude/skills/Journal/tools/journelly-location-weather.el
@@ -0,0 +1,257 @@
+;;; journelly-location-weather.el --- Location and weather helpers for Journelly -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Vincent Demeester
+
+;; Author: Vincent Demeester <vincent@demeester.fr>
+;; Keywords: org-mode, journelly, location, weather
+;; Version: 1.0.0
+
+;;; Commentary:
+
+;; Emacs Lisp functions to get location and weather data for Journelly journal entries.
+;;
+;; Location:
+;; - Uses IP-based geolocation (ipinfo.io)
+;; - Returns city name and GPS coordinates
+;; - Caches results for 1 hour
+;;
+;; Weather:
+;; - Uses wttr.in weather service
+;; - Returns temperature, condition, and iOS SF Symbol
+;; - Caches results for 30 minutes
+;; - Intelligent day/night symbol mapping
+;;
+;; Functions:
+;; - journelly-get-location: Get current location via IP geolocation
+;; - journelly-get-weather: Get current weather
+;; - journelly-batch-get-location: Batch mode wrapper for location
+;; - journelly-batch-get-weather: Batch mode wrapper for weather
+;;
+;; Usage (batch mode):
+;;   emacs --batch \
+;;     --load journelly-location-weather.el \
+;;     --eval "(journelly-batch-get-location)"
+;;
+;;   emacs --batch \
+;;     --load journelly-location-weather.el \
+;;     --eval "(journelly-batch-get-weather)"
+
+;;; Code:
+
+(require 'url)
+(require 'json)
+
+;;; Configuration
+
+(defvar journelly-cache-dir
+  (expand-file-name "journal" (or (getenv "XDG_CACHE_HOME")
+                                   (expand-file-name ".cache" "~")))
+  "Directory for caching location and weather data.")
+
+(defvar journelly-location-cache-timeout 3600
+  "Location cache timeout in seconds (default: 1 hour).")
+
+(defvar journelly-weather-cache-timeout 1800
+  "Weather cache timeout in seconds (default: 30 minutes).")
+
+;;; Utility functions
+
+(defun journelly--ensure-cache-dir ()
+  "Ensure cache directory exists."
+  (unless (file-exists-p journelly-cache-dir)
+    (make-directory journelly-cache-dir t)))
+
+(defun journelly--cache-file (key)
+  "Get cache file path for KEY."
+  (expand-file-name (format "%s.json" key) journelly-cache-dir))
+
+(defun journelly--cache-valid-p (cache-file timeout)
+  "Check if CACHE-FILE is valid within TIMEOUT seconds."
+  (when (file-exists-p cache-file)
+    (let* ((file-time (nth 5 (file-attributes cache-file)))
+           (current-time (current-time))
+           (age (float-time (time-subtract current-time file-time))))
+      (< age timeout))))
+
+(defun journelly--read-cache (cache-file)
+  "Read JSON data from CACHE-FILE."
+  (when (file-exists-p cache-file)
+    (with-temp-buffer
+      (insert-file-contents cache-file)
+      (goto-char (point-min))
+      (json-read))))
+
+(defun journelly--write-cache (cache-file data)
+  "Write DATA as JSON to CACHE-FILE."
+  (journelly--ensure-cache-dir)
+  (with-temp-file cache-file
+    (insert (json-encode data))))
+
+(defun journelly--fetch-url (url)
+  "Fetch URL and return parsed JSON response."
+  (let ((url-request-method "GET")
+        (url-request-extra-headers '(("User-Agent" . "Emacs/journelly"))))
+    (with-current-buffer (url-retrieve-synchronously url t nil 10)
+      (goto-char (point-min))
+      ;; Skip HTTP headers
+      (re-search-forward "^$")
+      (forward-line)
+      (let ((json-data (json-read)))
+        (kill-buffer)
+        json-data))))
+
+(defun journelly--is-night-p ()
+  "Return t if current time is night (20:00-06:00)."
+  (let ((hour (string-to-number (format-time-string "%H"))))
+    (or (>= hour 20) (< hour 6))))
+
+;;; Location functions
+
+(defun journelly--map-weather-symbol (description &optional is-night)
+  "Map weather DESCRIPTION to iOS SF Symbol name.
+If IS-NIGHT is non-nil, return night-appropriate symbols."
+  (let ((desc (downcase description)))
+    (if is-night
+        ;; Night conditions
+        (cond
+         ((string-match-p "\\(clear\\|sunny\\)" desc) "moon.stars")
+         ((string-match-p "partly.*cloud" desc) "cloud.moon")
+         ((string-match-p "\\(rain\\|drizzle\\|shower\\)" desc) "cloud.moon.rain")
+         (t "cloud.moon"))
+      ;; Day conditions
+      (cond
+       ((string-match-p "\\(clear\\|sunny\\)" desc) "sun.max")
+       ((string-match-p "partly.*cloud" desc) "cloud.sun")
+       ((string-match-p "\\(cloudy\\|overcast\\)" desc) "cloud")
+       ((string-match-p "heavy.*rain" desc) "cloud.heavyrain")
+       ((string-match-p "\\(rain\\|shower\\)" desc) "cloud.rain")
+       ((string-match-p "\\(drizzle\\|light.*rain\\)" desc) "cloud.drizzle")
+       ((string-match-p "snow" desc) "cloud.snow")
+       ((string-match-p "sleet" desc) "cloud.sleet")
+       ((string-match-p "\\(fog\\|mist\\)" desc) "cloud.fog")
+       ((string-match-p "\\(haze\\|smoke\\)" desc) "smoke")
+       ((string-match-p "wind" desc) "wind")
+       ((string-match-p "\\(thunder\\|storm\\)" desc) "cloud.bolt")
+       (t "cloud")))))
+
+(defun journelly-get-location (&optional no-cache)
+  "Get current location via IP geolocation.
+Returns alist with city, latitude, and longitude.
+If NO-CACHE is non-nil, fetch fresh data ignoring cache."
+  (let ((cache-file (journelly--cache-file "location")))
+    (if (and (not no-cache)
+             (journelly--cache-valid-p cache-file journelly-location-cache-timeout))
+        ;; Return cached data
+        (journelly--read-cache cache-file)
+      ;; Fetch fresh data
+      (let* ((response (journelly--fetch-url "https://ipinfo.io/json"))
+             (city (cdr (assoc 'city response)))
+             (loc (cdr (assoc 'loc response)))
+             (coords (when loc (split-string loc ",")))
+             (lat (when coords (car coords)))
+             (lon (when coords (cadr coords)))
+             (data `((city . ,(or city "Unknown"))
+                    (lat . ,(or lat "0"))
+                    (lon . ,(or lon "0")))))
+        ;; Cache the result
+        (journelly--write-cache cache-file data)
+        data))))
+
+(defun journelly-get-weather (&optional location no-cache)
+  "Get current weather for LOCATION (city name or coordinates).
+If LOCATION is nil, uses current location via IP.
+Returns alist with temperature, condition, and symbol.
+If NO-CACHE is non-nil, fetch fresh data ignoring cache."
+  (let* ((loc (or location ""))
+         (cache-key (if (string-empty-p loc) "weather-auto" (format "weather-%s" loc)))
+         (cache-file (journelly--cache-file cache-key)))
+    (if (and (not no-cache)
+             (journelly--cache-valid-p cache-file journelly-weather-cache-timeout))
+        ;; Return cached data
+        (journelly--read-cache cache-file)
+      ;; Fetch fresh data
+      (let* ((url (if (string-empty-p loc)
+                     "https://wttr.in/?format=j1"
+                   (format "https://wttr.in/%s?format=j1" (url-hexify-string loc))))
+             (response (journelly--fetch-url url))
+             (current (aref (cdr (assoc 'current_condition response)) 0))
+             (temp-c (cdr (assoc 'temp_C current)))
+             (weather-desc-array (cdr (assoc 'weatherDesc current)))
+             (weather-desc (cdr (assoc 'value (aref weather-desc-array 0))))
+             (temperature (format "%s°C" temp-c))
+             (is-night (journelly--is-night-p))
+             (symbol (journelly--map-weather-symbol weather-desc is-night))
+             (data `((temperature . ,temperature)
+                    (condition . ,weather-desc)
+                    (symbol . ,symbol))))
+        ;; Cache the result
+        (journelly--write-cache cache-file data)
+        data))))
+
+;;; Batch mode functions
+
+(defun journelly-batch-get-location (&optional format no-cache)
+  "Batch mode: Get location and print to stdout.
+FORMAT can be: json (default), city, coords, lat, lon, or all.
+If NO-CACHE is non-nil, ignore cache."
+  (let* ((format-type (or format "json"))
+         (data (journelly-get-location no-cache))
+         (city (cdr (assoc 'city data)))
+         (lat (cdr (assoc 'lat data)))
+         (lon (cdr (assoc 'lon data))))
+    (cond
+     ((string= format-type "json")
+      (princ (json-encode data))
+      (terpri))
+     ((string= format-type "city")
+      (princ city)
+      (terpri))
+     ((string= format-type "coords")
+      (princ (format "%s,%s" lat lon))
+      (terpri))
+     ((string= format-type "lat")
+      (princ lat)
+      (terpri))
+     ((string= format-type "lon")
+      (princ lon)
+      (terpri))
+     ((string= format-type "all")
+      (princ (format "%s (%s,%s)" city lat lon))
+      (terpri))
+     (t
+      (error "Unknown format: %s" format-type)))))
+
+(defun journelly-batch-get-weather (&optional location format no-cache)
+  "Batch mode: Get weather and print to stdout.
+LOCATION is optional city name or coordinates.
+FORMAT can be: json (default), temperature, condition, symbol, or all.
+If NO-CACHE is non-nil, ignore cache."
+  (let* ((format-type (or format "json"))
+         (data (journelly-get-weather location no-cache))
+         (temperature (cdr (assoc 'temperature data)))
+         (condition (cdr (assoc 'condition data)))
+         (symbol (cdr (assoc 'symbol data))))
+    (cond
+     ((string= format-type "json")
+      (princ (json-encode data))
+      (terpri))
+     ((string= format-type "temperature")
+      (princ temperature)
+      (terpri))
+     ((string= format-type "condition")
+      (princ condition)
+      (terpri))
+     ((string= format-type "symbol")
+      (princ symbol)
+      (terpri))
+     ((string= format-type "all")
+      (princ (format "%s %s (%s)" temperature condition symbol))
+      (terpri))
+     (t
+      (error "Unknown format: %s" format-type)))))
+
+;;; Provide
+
+(provide 'journelly-location-weather)
+
+;;; journelly-location-weather.el ends here
dots/.config/claude/skills/Journal/tools/journelly-manager
@@ -0,0 +1,400 @@
+#!/usr/bin/env bash
+# journelly-manager - CLI tool for Journelly journal file manipulation via Emacs batch mode
+# Copyright (C) 2025 Vincent Demeester
+# Part of Claude Code Journal skill
+
+set -euo pipefail
+
+# Configuration
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+BATCH_FUNCTIONS="${BATCH_FUNCTIONS:-$SCRIPT_DIR/journelly-batch-functions.el}"
+EMACS="${EMACS:-emacs}"
+
+# Debug mode
+DEBUG="${DEBUG:-0}"
+
+# Colors for output (if not outputting JSON)
+if [[ -t 1 ]] && [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
+    RED='\033[0;31m'
+    GREEN='\033[0;32m'
+    YELLOW='\033[1;33m'
+    BLUE='\033[0;34m'
+    NC='\033[0m' # No Color
+else
+    RED=''
+    GREEN=''
+    YELLOW=''
+    BLUE=''
+    NC=''
+fi
+
+# Error handling
+error() {
+    echo -e "${RED}Error: $*${NC}" >&2
+    exit 1
+}
+
+debug() {
+    if [[ "$DEBUG" == "1" ]]; then
+        echo -e "${YELLOW}Debug: $*${NC}" >&2
+    fi
+}
+
+info() {
+    if [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
+        echo -e "${BLUE}$*${NC}" >&2
+    fi
+}
+
+success() {
+    if [[ "${JSON_OUTPUT:-1}" != "1" ]]; then
+        echo -e "${GREEN}$*${NC}" >&2
+    fi
+}
+
+# Check dependencies
+check_deps() {
+    if ! command -v "$EMACS" &> /dev/null; then
+        error "Emacs not found. Set EMACS environment variable or install emacs."
+    fi
+
+    if [[ ! -f "$BATCH_FUNCTIONS" ]]; then
+        error "journelly-batch-functions.el not found at: $BATCH_FUNCTIONS"
+    fi
+}
+
+# Run Emacs batch command
+run_batch() {
+    local function_call="$1"
+    debug "Running: $EMACS --batch --load \"$BATCH_FUNCTIONS\" --eval \"$function_call\""
+
+    "$EMACS" --batch \
+        --load "$BATCH_FUNCTIONS" \
+        --eval "$function_call" 2>&1
+}
+
+# Usage information
+usage() {
+    cat <<EOF
+journelly-manager - CLI tool for managing Journelly journal files
+
+USAGE:
+    journelly-manager <command> [arguments]
+
+COMMANDS:
+    create FILE LOCATION CONTENT [options]
+        Create new journal entry
+
+        Options:
+            --latitude=LAT         GPS latitude
+            --longitude=LON        GPS longitude
+            --temperature=TEMP     Temperature (e.g., "15,2°C")
+            --condition=COND       Weather condition (e.g., "Cloudy")
+            --symbol=SYM           Weather symbol (e.g., "cloud")
+            --content-file=PATH    Read content from file instead
+
+        Examples:
+            journelly-manager create ~/desktop/org/Journelly.org "Home" "Today was great"
+
+            journelly-manager create ~/desktop/org/Journelly.org "Kyushu" \\
+                "Work session notes" \\
+                --latitude=48.8672 --longitude=2.1851 \\
+                --temperature="15,2°C" --condition="Partly Cloudy" --symbol="cloud.sun"
+
+    append FILE CONTENT [--content-file=PATH]
+        Append to today's journal entry
+
+        Examples:
+            journelly-manager append ~/desktop/org/Journelly.org "Additional thoughts"
+            journelly-manager append ~/desktop/org/Journelly.org --content-file=/tmp/notes.txt
+
+    list FILE [--limit=N]
+        List recent journal entries (default: 10)
+
+        Examples:
+            journelly-manager list ~/desktop/org/Journelly.org
+            journelly-manager list ~/desktop/org/Journelly.org --limit=20
+
+    search FILE QUERY
+        Search journal entries for text
+
+        Examples:
+            journelly-manager search ~/desktop/org/Journelly.org "claude"
+            journelly-manager search ~/desktop/org/Journelly.org "homelab"
+
+    get FILE DATE [TIME]
+        Get specific entry by date and optional time
+
+        Examples:
+            journelly-manager get ~/desktop/org/Journelly.org "2025-12-08"
+            journelly-manager get ~/desktop/org/Journelly.org "2025-12-08" "15:30"
+
+GLOBAL OPTIONS:
+    --help, -h             Show this help message
+    --version              Show version information
+    --debug                Enable debug output
+
+ENVIRONMENT VARIABLES:
+    EMACS                  Path to Emacs binary (default: emacs)
+    BATCH_FUNCTIONS        Path to journelly-batch-functions.el
+    DEBUG                  Enable debug mode (1 or 0)
+    JSON_OUTPUT            Output JSON only (1 or 0)
+
+EXAMPLES:
+    # Create simple entry
+    journelly-manager create ~/desktop/org/Journelly.org "Home" \\
+        "Productive day working on Claude skills."
+
+    # Create entry with weather data
+    journelly-manager create ~/desktop/org/Journelly.org "Rue Jean Bourguignon" \\
+        "Evening reflection" \\
+        --latitude=48.86721 --longitude=2.18509 \\
+        --temperature="8,5°C" --condition="Clear" --symbol="moon.stars"
+
+    # Create entry with content from file
+    echo "My journal thoughts..." > /tmp/entry.txt
+    journelly-manager create ~/desktop/org/Journelly.org "Kyushu" \\
+        --content-file=/tmp/entry.txt
+
+    # Append to today's entry
+    journelly-manager append ~/desktop/org/Journelly.org \\
+        "Update: Made more progress on the project!"
+
+    # List recent entries
+    journelly-manager list ~/desktop/org/Journelly.org --limit=5
+
+    # Search for entries about work
+    journelly-manager search ~/desktop/org/Journelly.org "work"
+
+    # Get specific entry
+    journelly-manager get ~/desktop/org/Journelly.org "2025-12-08" "15:30"
+
+VERSION:
+    1.0.0
+
+AUTHOR:
+    Vincent Demeester <vincent@demeester.fr>
+
+SEE ALSO:
+    Journelly iOS App: https://journelly.com
+    Org-mode: https://orgmode.org
+EOF
+}
+
+# Parse command line arguments
+parse_args() {
+    local cmd="${1:-}"
+    shift || true
+
+    case "$cmd" in
+        create)
+            cmd_create "$@"
+            ;;
+        append)
+            cmd_append "$@"
+            ;;
+        list)
+            cmd_list "$@"
+            ;;
+        search)
+            cmd_search "$@"
+            ;;
+        get)
+            cmd_get "$@"
+            ;;
+        --help|-h|help)
+            usage
+            exit 0
+            ;;
+        --version)
+            echo "journelly-manager 1.0.0"
+            exit 0
+            ;;
+        "")
+            error "No command specified. Use --help for usage."
+            ;;
+        *)
+            error "Unknown command: $cmd. Use --help for usage."
+            ;;
+    esac
+}
+
+# Command: create
+cmd_create() {
+    local file="${1:-}"
+    local location="${2:-}"
+    local content="${3:-}"
+    shift 3 || error "create requires FILE LOCATION CONTENT arguments"
+
+    [[ -z "$file" ]] && error "FILE argument required"
+    [[ -z "$location" ]] && error "LOCATION argument required"
+    [[ ! -f "$file" ]] && error "File not found: $file"
+
+    # Parse optional arguments
+    local latitude=""
+    local longitude=""
+    local temperature=""
+    local condition=""
+    local symbol=""
+    local content_file=""
+
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --latitude=*)
+                latitude="${1#*=}"
+                ;;
+            --longitude=*)
+                longitude="${1#*=}"
+                ;;
+            --temperature=*)
+                temperature="${1#*=}"
+                ;;
+            --condition=*)
+                condition="${1#*=}"
+                ;;
+            --symbol=*)
+                symbol="${1#*=}"
+                ;;
+            --content-file=*)
+                content_file="${1#*=}"
+                [[ ! -f "$content_file" ]] && error "Content file not found: $content_file"
+                ;;
+            *)
+                error "Unknown option: $1"
+                ;;
+        esac
+        shift
+    done
+
+    # Build Emacs Lisp call
+    local elisp_call="(journelly-batch-create-entry \"$file\" \"$location\" \"$content\""
+
+    [[ -n "$latitude" ]] && elisp_call="$elisp_call \"$latitude\"" || elisp_call="$elisp_call nil"
+    [[ -n "$longitude" ]] && elisp_call="$elisp_call \"$longitude\"" || elisp_call="$elisp_call nil"
+    [[ -n "$temperature" ]] && elisp_call="$elisp_call \"$temperature\"" || elisp_call="$elisp_call nil"
+    [[ -n "$condition" ]] && elisp_call="$elisp_call \"$condition\"" || elisp_call="$elisp_call nil"
+    [[ -n "$symbol" ]] && elisp_call="$elisp_call \"$symbol\"" || elisp_call="$elisp_call nil"
+    [[ -n "$content_file" ]] && elisp_call="$elisp_call \"$content_file\"" || elisp_call="$elisp_call nil"
+
+    elisp_call="$elisp_call)"
+
+    info "Creating journal entry..."
+    local result
+    result=$(run_batch "$elisp_call")
+    echo "$result"
+
+    if echo "$result" | grep -q '"success":true'; then
+        success "Journal entry created successfully"
+    fi
+}
+
+# Command: append
+cmd_append() {
+    local file="${1:-}"
+    local content="${2:-}"
+    shift 2 || error "append requires FILE CONTENT arguments"
+
+    [[ -z "$file" ]] && error "FILE argument required"
+    [[ ! -f "$file" ]] && error "File not found: $file"
+
+    local content_file=""
+
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --content-file=*)
+                content_file="${1#*=}"
+                [[ ! -f "$content_file" ]] && error "Content file not found: $content_file"
+                ;;
+            *)
+                error "Unknown option: $1"
+                ;;
+        esac
+        shift
+    done
+
+    local elisp_call="(journelly-batch-append-to-today \"$file\" \"$content\""
+    [[ -n "$content_file" ]] && elisp_call="$elisp_call \"$content_file\"" || elisp_call="$elisp_call nil"
+    elisp_call="$elisp_call)"
+
+    info "Appending to today's entry..."
+    local result
+    result=$(run_batch "$elisp_call")
+    echo "$result"
+
+    if echo "$result" | grep -q '"success":true'; then
+        success "Content appended successfully"
+    fi
+}
+
+# Command: list
+cmd_list() {
+    local file="${1:-}"
+    shift || error "list requires FILE argument"
+
+    [[ -z "$file" ]] && error "FILE argument required"
+    [[ ! -f "$file" ]] && error "File not found: $file"
+
+    local limit=""
+
+    while [[ $# -gt 0 ]]; do
+        case "$1" in
+            --limit=*)
+                limit="${1#*=}"
+                ;;
+            *)
+                error "Unknown option: $1"
+                ;;
+        esac
+        shift
+    done
+
+    local elisp_call="(journelly-batch-list-entries \"$file\""
+    [[ -n "$limit" ]] && elisp_call="$elisp_call \"$limit\"" || elisp_call="$elisp_call nil"
+    elisp_call="$elisp_call)"
+
+    info "Listing recent entries..."
+    run_batch "$elisp_call"
+}
+
+# Command: search
+cmd_search() {
+    local file="${1:-}"
+    local query="${2:-}"
+    shift 2 || error "search requires FILE QUERY arguments"
+
+    [[ -z "$file" ]] && error "FILE argument required"
+    [[ -z "$query" ]] && error "QUERY argument required"
+    [[ ! -f "$file" ]] && error "File not found: $file"
+
+    local elisp_call="(journelly-batch-search \"$file\" \"$query\")"
+
+    info "Searching for: $query"
+    run_batch "$elisp_call"
+}
+
+# Command: get
+cmd_get() {
+    local file="${1:-}"
+    local date="${2:-}"
+    local time="${3:-}"
+    shift 2 || error "get requires FILE DATE arguments"
+
+    [[ -z "$file" ]] && error "FILE argument required"
+    [[ -z "$date" ]] && error "DATE argument required"
+    [[ ! -f "$file" ]] && error "File not found: $file"
+
+    local elisp_call="(journelly-batch-get-entry \"$file\" \"$date\""
+    [[ -n "$time" ]] && elisp_call="$elisp_call \"$time\"" || elisp_call="$elisp_call nil"
+    elisp_call="$elisp_call)"
+
+    info "Getting entry for: $date${time:+ at $time}"
+    run_batch "$elisp_call"
+}
+
+# Main
+main() {
+    check_deps
+    parse_args "$@"
+}
+
+main "$@"
dots/.config/claude/skills/Journal/README.md
@@ -0,0 +1,162 @@
+# Journal Skill
+
+Claude Code skill for managing Journelly-format journal entries.
+
+## Overview
+
+This skill enables Claude to create and manage journal entries in the Journelly org-mode format. Journelly is an iOS app that stores journal entries as org-mode headings in a single file with optional GPS/weather metadata.
+
+## Components
+
+### SKILL.md
+The skill definition and documentation. Contains:
+- Journelly format specification
+- Usage examples
+- Best practices
+- Integration patterns
+
+### tools/journelly-batch-functions.el
+Emacs Lisp batch functions for programmatic journal manipulation:
+- `journelly-batch-create-entry` - Create new entry
+- `journelly-batch-append-to-today` - Append to today's entry
+- `journelly-batch-list-entries` - List recent entries
+- `journelly-batch-search` - Search content
+- `journelly-batch-get-entry` - Get specific entry by date
+
+### tools/journelly-manager
+Bash CLI wrapper around the Emacs batch functions. Provides a user-friendly interface for all journal operations.
+
+## Usage
+
+### Via Claude
+
+Simply ask Claude to create journal entries:
+
+```
+"Create a journal entry about today's work on the Journal skill"
+"Add to today's journal that I finished the implementation"
+"Search my journal for entries about homelab"
+"Show me my last 5 journal entries"
+```
+
+### Direct CLI Usage
+
+```bash
+# Create entry
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Home" "Today was productive!"
+
+# Create with weather
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Kyushu" "Work notes" \
+  --latitude=48.8672 --longitude=2.1851 \
+  --temperature="15,2°C" --condition="Cloudy" --symbol="cloud"
+
+# Append to today
+~/.config/claude/skills/Journal/tools/journelly-manager append \
+  ~/desktop/org/Journelly.org "Additional thoughts for the day"
+
+# List recent entries
+~/.config/claude/skills/Journal/tools/journelly-manager list \
+  ~/desktop/org/Journelly.org --limit=5
+
+# Search
+~/.config/claude/skills/Journal/tools/journelly-manager search \
+  ~/desktop/org/Journelly.org "claude"
+
+# Get specific entry
+~/.config/claude/skills/Journal/tools/journelly-manager get \
+  ~/desktop/org/Journelly.org "2025-12-08"
+```
+
+## Journelly Format
+
+Journal entries follow this structure:
+
+```org
+* [YYYY-MM-DD Day HH:MM] @ Location
+:PROPERTIES:
+:LATITUDE: 48.86721377062119
+:LONGITUDE: 2.1850910842231994
+:WEATHER_TEMPERATURE: 5,8°C
+:WEATHER_CONDITION: Cloudy
+:WEATHER_SYMBOL: cloud
+:END:
+Entry content goes here...
+
+- Section Title (use indented lists, NOT sub-headings)
+  - Item 1
+  - Item 2
+```
+
+- **Reverse chronological**: Newest entries at top
+- **Optional properties**: GPS/weather metadata from iOS app
+- **Content format**: Org-mode support (lists, links, code blocks)
+- **NO sub-headings**: Use indented lists instead of `**` level 2 headings
+- **Single file**: All entries in `~/desktop/org/Journelly.org`
+
+## Integration
+
+### With Journelly iOS App
+- Primary mobile interface for journal
+- Automatically adds GPS and weather data
+- Syncs via iCloud
+- Photos captured on iPhone
+
+### With Claude Code
+- Create entries via natural language
+- Search and review past entries
+- Append to existing entries
+- Generate journal entries from work sessions
+
+### With Syncthing
+- Sync across devices
+- Available on desktop and mobile
+- Changes sync bidirectionally
+
+## Testing
+
+Test the journelly-manager tool:
+
+```bash
+# Show help
+~/.config/claude/skills/Journal/tools/journelly-manager --help
+
+# List entries (read-only)
+~/.config/claude/skills/Journal/tools/journelly-manager list \
+  ~/desktop/org/Journelly.org --limit=3
+
+# Search entries (read-only)
+~/.config/claude/skills/Journal/tools/journelly-manager search \
+  ~/desktop/org/Journelly.org "test"
+```
+
+## Development
+
+The skill uses Emacs batch mode for reliable org-mode parsing and manipulation:
+
+1. **journelly-batch-functions.el**: Core logic in Emacs Lisp
+2. **journelly-manager**: Bash wrapper for CLI interface
+3. **SKILL.md**: Documentation for Claude integration
+
+All operations return JSON for programmatic integration.
+
+## Related Skills
+
+- **Notes**: Denote-format note-taking (multi-file, topic-based)
+- **TODOs**: Task management with org-mode
+- **Org**: Core org-mode manipulation library
+
+The Journal skill complements these by providing time-based, personal reflections while Notes provides topic-based knowledge management.
+
+## Version
+
+1.0.0
+
+## Author
+
+Vincent Demeester <vincent@demeester.fr>
+
+## License
+
+Part of personal Claude Code configuration.
dots/.config/claude/skills/Journal/SKILL.md
@@ -0,0 +1,562 @@
+---
+name: Journal
+description: Journelly-format journal entries. USE WHEN user wants to create journal entries, write reflections, or work with Journelly.org file.
+---
+
+# Journal Writing Workflow
+
+## Purpose
+Assist with creating and managing journal entries in Journelly format using org-mode.
+
+### Context Detection
+
+**This skill activates when:**
+- User asks to create a journal entry, write a journal, or add to journal
+- User mentions Journelly, journaling, or daily reflections
+- User references `Journelly.org` file
+- User asks about logging thoughts, daily notes, or personal reflections
+
+## Journal Location
+**Journal file**: `~/desktop/org/Journelly.org`
+
+## Journelly Format
+
+Journelly is an iOS app that stores journal entries in org-mode format as a single file with entries in reverse chronological order (newest first).
+
+### Entry Structure
+
+**Basic entry format**:
+```org
+* [YYYY-MM-DD Day HH:MM] @ Location
+:PROPERTIES:
+:LATITUDE: 48.86721377062119
+:LONGITUDE: 2.1850910842231994
+:WEATHER_TEMPERATURE: 5,8°C
+:WEATHER_CONDITION: Cloudy
+:WEATHER_SYMBOL: cloud
+:END:
+Entry content goes here...
+
+Can have multiple paragraphs.
+
+- Lists work
+- [ ] Checkboxes work
+- [X] Completed items
+
+Images can be included:
+[[file:Journelly.org.assets/images/IMAGE.jpeg]]
+```
+
+**Entry without properties**:
+```org
+* [YYYY-MM-DD Day HH:MM] @ Location
+
+Simple entry without weather/GPS metadata.
+```
+
+### Key Components
+
+**Heading**:
+- Format: `* [YYYY-MM-DD Day HH:MM] @ Location`
+- Timestamp: Full date and time in org-mode format
+- Location: Can be a place name, address, or general location
+- Examples:
+  - `* [2025-12-08 Mon 15:30] @ Home`
+  - `* [2025-12-08 Mon 09:15] @ Kyushu`
+  - `* [2025-12-08 Mon 22:00] @ Rue Jean Bourguignon`
+
+**Properties drawer** (optional):
+- `:LATITUDE:` - GPS latitude
+- `:LONGITUDE:` - GPS longitude
+- `:WEATHER_TEMPERATURE:` - Temperature with unit (e.g., `5,8°C`)
+- `:WEATHER_CONDITION:` - Weather description (e.g., `Cloudy`, `Clear`, `Partly Cloudy`)
+- `:WEATHER_SYMBOL:` - Icon symbol (e.g., `cloud`, `sun.max`, `cloud.moon`)
+
+**Content**:
+- Free-form text in org-mode format
+- Support for org-mode features: lists, checkboxes, links, code blocks
+- Images stored in `Journelly.org.assets/images/` directory
+- Can include hashtags (e.g., `#lang_en`)
+- Can reference other org files with org-mode links
+
+**IMPORTANT - Content Formatting Restrictions**:
+- **NO sub-headings**: Journelly does NOT support `**` level 2 headings or any sub-headings within entries
+- Use **indented lists** instead of sub-headings for structure:
+  ```org
+  - Section Title
+    - Item 1
+    - Item 2
+  ```
+- For bold emphasis in list items, use text formatting: `- *Bold Section Title*`
+- Each journal entry must be a single `*` level 1 heading - no nested headings allowed
+
+### Entry Order
+
+Entries are in **reverse chronological order** - newest entries at the top of the file, after the header.
+
+## File Header
+
+The Journelly.org file starts with:
+```org
+#+TITLE: My Journal (via https://journelly.com)
+#+STARTUP: showall
+:journelly:
+:doc_version: 1.0
+:end:
+```
+
+This header should never be modified.
+
+## Creating Entries
+
+### Automatic Location and Weather Detection
+
+Helper scripts are available to automatically get current location and weather:
+
+**Get location (IP-based geolocation)**:
+```bash
+# Get city and coordinates
+~/.config/claude/skills/Journal/tools/get-location
+# Output: Saint-Denis (48.9356,2.3539)
+
+# Get just coordinates
+~/.config/claude/skills/Journal/tools/get-location --coords
+# Output: 48.9356,2.3539
+
+# Get as JSON
+~/.config/claude/skills/Journal/tools/get-location --json
+# Output: {"city":"Saint-Denis","lat":"48.9356","lon":"2.3539"}
+```
+
+**Get weather (from wttr.in)**:
+```bash
+# Get current weather
+~/.config/claude/skills/Journal/tools/get-weather
+# Output: 15°C Partly cloudy (cloud.sun)
+
+# Get just temperature
+~/.config/claude/skills/Journal/tools/get-weather --temperature
+# Output: 15°C
+
+# Get as JSON
+~/.config/claude/skills/Journal/tools/get-weather --json
+# Output: {"temperature":"15°C","condition":"Partly cloudy","symbol":"cloud.sun"}
+
+# Get weather for specific location
+~/.config/claude/skills/Journal/tools/get-weather Paris
+```
+
+**Notes**:
+- Location uses IP-based geolocation (city-level accuracy, no GPS hardware required)
+- Weather uses wttr.in service (no API key required)
+- Both are cached (location: 1 hour, weather: 30 minutes) to reduce API calls
+- Symbols are mapped to iOS SF Symbols for consistency with Journelly app
+
+### Using journelly-manager (Recommended)
+
+The `journelly-manager` tool provides batch mode operations for creating journal entries:
+
+```bash
+# Create simple entry with just location
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Kyushu" "Entry content here"
+
+# Create entry with weather/GPS metadata
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Rue Jean Bourguignon" "Entry content" \
+  --latitude=48.86721 \
+  --longitude=2.18509 \
+  --temperature="15,2°C" \
+  --condition="Partly Cloudy" \
+  --symbol="cloud.sun"
+
+# Create entry with content from file
+echo "My thoughts today..." > /tmp/journal.txt
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Home" --content-file=/tmp/journal.txt
+
+# Append to today's entry (if one exists from today)
+~/.config/claude/skills/Journal/tools/journelly-manager append \
+  ~/desktop/org/Journelly.org "Additional thoughts for today"
+
+# Create entry with automatic location and weather
+LOC_DATA=$(~/.config/claude/skills/Journal/tools/get-location --json)
+WEATHER_DATA=$(~/.config/claude/skills/Journal/tools/get-weather --json)
+
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org \
+  "$(echo "$LOC_DATA" | jq -r .city)" \
+  "Today's reflection with automatic metadata" \
+  --latitude="$(echo "$LOC_DATA" | jq -r .lat)" \
+  --longitude="$(echo "$LOC_DATA" | jq -r .lon)" \
+  --temperature="$(echo "$WEATHER_DATA" | jq -r .temperature)" \
+  --condition="$(echo "$WEATHER_DATA" | jq -r .condition)" \
+  --symbol="$(echo "$WEATHER_DATA" | jq -r .symbol)"
+```
+
+**Output**: Returns JSON with success status and entry details.
+
+### Manual Creation
+
+If creating entries manually:
+
+1. Open `~/desktop/org/Journelly.org`
+2. After the file header (after `:end:`), insert new entry at the top
+3. Use timestamp format: `[YYYY-MM-DD Day HH:MM]`
+4. Add location after `@`
+5. Optionally add PROPERTIES drawer
+6. Write content below
+
+**Get current timestamp**:
+```bash
+# Org-mode format
+date +"[%Y-%m-%d %a %H:%M]"
+```
+
+## Entry Examples
+
+### Structured entry with indented lists (RECOMMENDED for complex entries):
+```org
+* [2025-12-10 Wed 17:05] @ Paris
+:PROPERTIES:
+:LATITUDE: 48.8534
+:LONGITUDE: 2.3488
+:END:
+Productive day with significant progress across multiple areas.
+
+- Work (Tekton/CI-CD)
+  - [X] Fixed workflow call in pipeline (priority 1)
+  - [X] Created issue for cherry-pick workflow
+  - [X] Standardized retest workflow across repositories
+
+- Infrastructure & Homelab
+  - [X] Set up Navidrome music streaming server
+  - [X] Implemented color-scheme switcher on kyushu
+  - [ ] Setup imap-filter (in progress)
+
+- Personal Productivity
+  - Updated imapfilter to archive emails by year
+  - Researched Everyday Systems habit formation
+  - Created note documenting implementation ideas
+
+10 completed tasks, feeling productive!
+```
+
+**Note**: Use indented lists (with 2 spaces) instead of sub-headings (`**`) - Journelly doesn't support nested headings!
+
+### Simple reflection:
+```org
+* [2025-12-08 Mon 15:30] @ Home
+
+Had a productive day working on Claude skills. The Journal skill
+is coming together nicely. Need to test the batch functions next.
+```
+
+### Work notes with checklist:
+```org
+* [2025-12-08 Mon 09:00] @ Kyushu
+
+Today's focus:
+- [X] Review pull requests
+- [X] Fix bug in pipeline
+- [ ] Write documentation
+- [ ] Team meeting at 14:00
+
+Making good progress on the telemetry work.
+```
+
+### Evening reflection with location data:
+```org
+* [2025-12-08 Mon 22:30] @ Rue Jean Bourguignon
+:PROPERTIES:
+:LATITUDE: 48.86721377062119
+:LONGITUDE: 2.1850910842231994
+:WEATHER_TEMPERATURE: 8,5°C
+:WEATHER_CONDITION: Clear
+:WEATHER_SYMBOL: moon.stars
+:END:
+Quiet evening. Spent time with family and worked on some personal
+projects. Feeling good about the progress this week.
+
+Tomorrow's priorities:
+- House hunting follow-up
+- Finish keyboard configuration
+- Review Rhea setup
+```
+
+### Vacation entry with photos:
+```org
+* [2025-08-16 Sat 04:40] @ Rue Jean Bourguignon
+:PROPERTIES:
+:LATITUDE: 48.868139960083184
+:LONGITUDE: 2.184074493463655
+:WEATHER_TEMPERATURE: 16,8°C
+:WEATHER_CONDITION: Clear
+:WEATHER_SYMBOL: moon.stars
+:END:
+Good day at Plaisir with Malek and Ilyan. Everyone was tired at
+the end but for good reasons.
+
+[[file:Journelly.org.assets/images/CDB4EC5D-153A-4C35-A7E8-17BA94F7FEBD.jpeg]]
+
+[[file:Journelly.org.assets/images/9CB95E09-CD4E-4868-BFE0-F0B129A8356B.jpeg]]
+
+We need to find a way to have less mosquitoes bites for Ayla because
+it is a lot and it is painful to see her with all thoses..
+```
+
+## Best Practices
+
+### When to Journal
+
+- **Morning**: Day planning, intentions, priorities
+- **During work**: Quick thoughts, decisions, blockers
+- **Evening**: Reflections, gratitude, learnings
+- **Anytime**: Significant moments, insights, emotions
+
+### Writing Guidelines
+
+- **Be authentic**: Write for yourself, not an audience
+- **Be specific**: Include details, context, emotions
+- **Be brief**: Don't overthink it, capture the moment
+- **Be consistent**: Regular entries build a valuable record
+- **Use org-mode features**: Lists, checkboxes, links enhance entries
+
+### Location Guidelines
+
+- Use recognizable place names for your context
+- Can be specific (address) or general (room name, city)
+- Examples:
+  - Work: `Kyushu` (machine name)
+  - Home: `Home`, `Rue Jean Bourguignon`
+  - Travel: `Amsterdam`, `R27`, `Boulevard de Turin`
+
+### Privacy Considerations
+
+- Journal contains personal thoughts and location data
+- Stored locally and synced via Syncthing/iCloud
+- No properties needed if you prefer not to log GPS/weather
+- You control what details to include
+
+## Common Use Cases
+
+### Daily Reflection
+```bash
+# Simple end-of-day reflection
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Home" \
+  "Productive day. Made progress on the Journal skill. \
+   Looking forward to testing it tomorrow."
+```
+
+### Work Log
+```bash
+# Log work accomplishments
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Kyushu" \
+  "Completed PR review. Fixed authentication bug. \
+   Team meeting went well - discussed Q1 roadmap."
+```
+
+### Quick Thought
+```bash
+# Capture an idea quickly
+~/.config/claude/skills/Journal/tools/journelly-manager create \
+  ~/desktop/org/Journelly.org "Home" \
+  "Idea: Create a dashboard for monitoring homelab services. \
+   Could use Grafana + Prometheus."
+```
+
+### Adding to Today's Entry
+```bash
+# Append to existing entry from today
+~/.config/claude/skills/Journal/tools/journelly-manager append \
+  ~/desktop/org/Journelly.org \
+  "Update: The dashboard idea is working! Initial prototype running."
+```
+
+## Integration with Other Systems
+
+### Journelly iOS App
+
+- Primary mobile interface for journal entries
+- Automatically adds GPS and weather data
+- Syncs via iCloud to make file available on Mac
+- Can include photos captured on iPhone
+- Creates proper org-mode format automatically
+
+### Syncthing
+
+- Journal file synced across devices
+- Available on desktop for reading/searching
+- Can edit from Emacs on desktop
+- Changes sync back to iOS app
+
+### Emacs
+
+- Read and search journal with org-mode commands
+- Use `org-sparse-tree` to filter entries
+- Export to other formats (HTML, PDF)
+- Integration with org-agenda if desired
+
+## Searching and Reviewing
+
+### Find Recent Entries
+```bash
+# Show last 10 entries (most recent)
+grep -n "^\* \[" ~/desktop/org/Journelly.org | head -10
+```
+
+### Search by Date
+```bash
+# Find entries from December 2025
+grep "^\* \[2025-12-" ~/desktop/org/Journelly.org
+
+# Find entries from specific day
+grep "^\* \[2025-12-08" ~/desktop/org/Journelly.org
+```
+
+### Search by Location
+```bash
+# Find all entries at Kyushu
+grep "@ Kyushu" ~/desktop/org/Journelly.org
+```
+
+### Search Content
+```bash
+# Find entries mentioning specific topic
+grep -i "claude" ~/desktop/org/Journelly.org
+
+# Context around matches
+grep -C 3 -i "homelab" ~/desktop/org/Journelly.org
+```
+
+### Search with Ripgrep
+```bash
+# Case-insensitive search with context
+rg -i "keyboard" ~/desktop/org/Journelly.org
+
+# Show only entries about work
+rg "@ Kyushu" ~/desktop/org/Journelly.org -A 10
+```
+
+## Journelly vs Notes
+
+**Use Journelly for**:
+- Daily reflections and thoughts
+- Personal experiences and emotions
+- Time-based chronicle of life
+- Quick captures without much structure
+- Location-tagged memories
+
+**Use Notes (denote) for**:
+- Technical documentation
+- Learning notes and research
+- Reference material
+- Project planning
+- Knowledge that needs retrieval by topic
+
+**They complement each other**: Journal captures the journey, Notes capture the knowledge.
+
+## Org-Mode Features in Entries
+
+### Lists
+```org
+* [2025-12-08 Mon 10:00] @ Home
+
+Today's agenda:
+- Morning: Focus work
+- Afternoon: Meetings
+- Evening: Family time
+```
+
+### Checkboxes
+```org
+* [2025-12-08 Mon 08:00] @ Kyushu
+
+Weekly goals:
+- [X] Complete feature implementation
+- [X] Review 5 PRs
+- [ ] Write documentation
+- [ ] Update roadmap
+```
+
+### Links
+```org
+* [2025-12-08 Mon 16:00] @ Home
+
+Referenced my [[file:~/desktop/org/notes/20251205T140000--nixos-config__nixos.org][NixOS config notes]]
+for today's work.
+
+Useful article: [[https://example.com][Link Title]]
+```
+
+### Code Blocks
+```org
+* [2025-12-08 Mon 14:00] @ Kyushu
+
+Found a useful command today:
+
+#+begin_src bash
+nix build .#package-name
+#+end_src
+
+This made the build process much faster.
+```
+
+### Tables
+```org
+* [2025-12-08 Mon 12:00] @ Home
+
+Tracking house options:
+
+| Address          | Price | Score |
+|------------------+-------+-------|
+| Rue Victor Hugo  | 720k  |   7/10|
+| Allée Perruchet  | 750k  |   9/10|
+```
+
+## Tips
+
+1. **Write regularly**: Even short entries build a valuable record
+2. **Don't overthink**: Capture thoughts as they come
+3. **Use location meaningfully**: Helps trigger memories later
+4. **Include context**: Future you will appreciate the details
+5. **Reference other files**: Link to notes, todos, or external resources
+6. **Use hashtags sparingly**: `#lang_en`, `#work`, `#personal` can help
+7. **Add photos when meaningful**: Visual memories are powerful
+8. **Review periodically**: Monthly or yearly reviews reveal patterns
+
+## Example Workflow
+
+### Mobile (Journelly iOS)
+1. Open Journelly app
+2. Tap to create new entry
+3. Write thoughts (voice dictation works)
+4. App automatically adds timestamp, location, weather
+5. Can attach photos from camera or library
+6. Entry syncs to iCloud → available on Mac
+
+### Desktop (Claude/Emacs)
+1. Ask Claude to create journal entry
+2. Claude uses `journelly-manager` to add entry
+3. Entry inserted at top of file (newest first)
+4. File syncs back via iCloud/Syncthing
+5. Entry appears in iOS app
+
+### Hybrid
+- Quick captures on mobile (always with you)
+- Longer reflections on desktop (better for typing)
+- Search and review on desktop (powerful tools)
+- Photos from mobile, text from either
+
+## Privacy & Sync
+
+- Journal is a plain text file you control
+- Location data is optional (from iOS app)
+- Synced via iCloud (encrypted in transit)
+- Can also use Syncthing for local-only sync
+- No third-party service has access to content
+- Backup with your regular backup strategy
+
+Remember: Your journal is for you. Write what helps you think, remember, and grow.