Commit 7e2904cb5c9a
Changed files (9)
dots
.config
claude
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.