gn
get_notes.md
bash — 80×24

gn (Get Notes) is a simple CLI note-taking tool for people who prefer plain text over platforms. It uses Markdown files, your existing $EDITOR, and a private GitHub repository to create a simple, portable notes tool that works in any Bash environment — even if you don't have git installed.

Open a note, write, save, and quit. gn handles the rest. It automatically pulls the latest version before you edit and pushes your changes when you're done via the GitHub API — no Git required, just curl. No databases, no subscriptions, no vendor lock-in — just text files and your terminal.

Prerequisites:

Before installing gn ensure your Github environment is set up.

A GitHub Account Setup

You need a GitHub account and a GitHub personal access token. Git is not required.

When creating the token, grant it the repo scope (full control of private repositories). Save the token - you'll need it in the configuration setup step below.

B Create a Private GitHub Repository

Log into GitHub, click New Repository, and name it gn. Ensure you explicitly change visibility to Private so your data stays hidden. Do not setup with a README, license, or .gitignore layout template.

Download gn

Download the automated installer or manually configure gn using the directions below.

Option A: Installer (Recommended)

Download an install.sh that prompts you for your GitHub credentials during installation.

Make Executable And Run

chmod +x install.sh && ./install.sh

Option B: Manual Set Up

#!/usr/bin/env bash

# --- Configuration ---
NOTES_DIR="$HOME/gn"

# --- Load Config ---
CONFIG_FILE="$NOTES_DIR/gn.conf"
if [ -f "$CONFIG_FILE" ]; then
    . "$CONFIG_FILE"
else
    echo "Error: No config found at $CONFIG_FILE"
    echo "Create it with:"
    echo "  GH_TOKEN=yourtoken"
    echo "  GH_OWNER=yourusername"
    echo "  GH_REPO=yourrepo"
    exit 1
fi

GH_API="https://api.github.com/repos/$GH_OWNER/$GH_REPO/contents"

mkdir -p "$NOTES_DIR"
cd "$NOTES_DIR" || { echo "Error: Could not access $NOTES_DIR"; exit 1; }

# --- Secure curl wrapper: token passed via tempfile, never exposed in ps ---
gh_curl() {
    local hdr
    hdr=$(mktemp)
    echo "Authorization: token $GH_TOKEN" > "$hdr"
    chmod 600 "$hdr"
    curl -s -H "@$hdr" "$@"
    local rc=$?
    rm -f "$hdr"
    return $rc
}

# --- GitHub API Sync Functions (used when git is not available) ---
pull_from_github() {
    local file="$1"
    local response content http_code
    response=$(gh_curl -w "\n%{http_code}" "$GH_API/$file")
    http_code=$(echo "$response" | tail -1)
    response=$(echo "$response" | head -n -1)
    if [ "$http_code" = "404" ]; then
        return 0
    fi
    if [ "$http_code" != "200" ]; then
        echo "Error: pull failed (HTTP $http_code). Check your token and repo name." >&2
        exit 1
    fi
    content=$(echo "$response" | grep '"content"' | sed 's/.*"content": *"\(.*\)".*/\1/' | tr -d '\\n')
    if [ -n "$content" ]; then
        echo "$content" | base64 -d > "$file" 2>/dev/null || echo "$content" | base64 -D > "$file"
    fi
}

push_to_github() {
    local file="$1"
    local sha content msg api_url sha_response push_response http_code
    api_url="$GH_API/$file"
    sha_response=$(gh_curl "$api_url")
    sha=$(echo "$sha_response" | grep '"sha"' | head -1 | sed 's/.*"sha": *"\([^"]*\)".*/\1/')
    content=$(base64 -w0 < "$file" 2>/dev/null || base64 < "$file")
    msg="Note update: $file on $(date '+%Y-%m-%d %H:%M:%S')"
    local sha_field=""
    [ -n "$sha" ] && sha_field=",\"sha\":\"$sha\""
    push_response=$(gh_curl -w "\n%{http_code}" -X PUT "$api_url" \
        -d "{\"message\":\"$msg\",\"content\":\"$content\"$sha_field}")
    http_code=$(echo "$push_response" | tail -1)
    if [ "$http_code" != "200" ] && [ "$http_code" != "201" ]; then
        echo "Error: push failed (HTTP $http_code). Your note was saved locally but not synced." >&2
        exit 1
    fi
}

# --- Help Text Function ---
show_help() {
    echo "Usage: gn [options] [note_name]"
    echo ""
    echo "Options:"
    echo "  -h        Show this help message"
    echo "  -l        List all notes in your notes directory"
    echo "  -g QUERY  Search for text across all notes (grep)"
    echo "  -t        Quickly open today's journal note (YYYY-MM-DD.md)"
    echo "  -d NOTE   Delete a note locally and from GitHub"
    echo "  -r OLD NEW  Rename a note locally and on GitHub"
    echo ""
    echo "Examples:"
    echo "  gn                  Opens index.md"
    echo "  gn log              Creates log.md"
    echo "  gn log              Opens log.md"
    echo "  gn work/todo        Opens work/todo.md"
    echo "  gn -g 'api key'     Searches all notes for the term 'api key'"
    echo "  gn -t               Creates a note named today's date"
    exit 0
}

# --- List Files Function ---
list_notes() {
    echo "Current Notes in $NOTES_DIR:"
    if [ -d "$NOTES_DIR" ]; then
        find . -type f -not -name "gn.conf" -not -path '*/.*' | sed 's|^\./||' | sort
    fi
    exit 0
}

# --- Search Inside Notes Function ---
search_notes() {
    echo "Searching for '$1' inside notes..."
    grep -Rin "$1" . --exclude-dir=".git" --exclude="gn.conf"
    exit 0
}

# --- Delete Note Function ---
delete_note() {
    local file="$1"
    if [[ "$file" != *.md ]]; then
        file="${file}.md"
    fi
    if [ ! -f "$NOTES_DIR/$file" ]; then
        echo "Error: '$file' not found locally."
        exit 1
    fi
    read -r -p "Delete '$file'? This cannot be undone. [y/N] " confirm
    [[ "$confirm" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; }
    local sha api_url
    api_url="$GH_API/$file"
    sha=$(gh_curl "$api_url" | grep '"sha"' | head -1 | sed 's/.*"sha": *"\([^"]*\)".*/\1/')
    if [ -n "$sha" ]; then
        gh_curl -X DELETE "$api_url" \
            -d "{\"message\":\"Delete $file\",\"sha\":\"$sha\"}" > /dev/null
        echo "Deleted from GitHub."
    else
        echo "Warning: File not found on GitHub. Removing locally only."
    fi
    rm "$NOTES_DIR/$file"
    echo "Deleted '$file'."
    exit 0
}

# --- Rename Note Function ---
rename_note() {
    local old_name="$1"
    local new_name="$2"
    if [[ "$old_name" != *.md ]]; then
        old_name="${old_name}.md"
    fi
    if [[ "$new_name" != *.md ]]; then
        new_name="${new_name}.md"
    fi
    if [ ! -f "$NOTES_DIR/$old_name" ]; then
        echo "Error: '$old_name' not found locally."
        exit 1
    fi
    if [ -f "$NOTES_DIR/$new_name" ]; then
        echo "Error: '$new_name' already exists."
        exit 1
    fi
    local sha old_api_url new_api_url content
    old_api_url="$GH_API/$old_name"
    new_api_url="$GH_API/$new_name"
    sha=$(gh_curl "$old_api_url" | grep '"sha"' | head -1 | sed 's/.*"sha": *"\([^"]*\)".*/\1/')
    content=$(base64 -w0 < "$NOTES_DIR/$old_name" 2>/dev/null || base64 < "$NOTES_DIR/$old_name")
    # Create new file on GitHub
    gh_curl -X PUT "$new_api_url" \
        -d "{\"message\":\"Rename $old_name to $new_name\",\"content\":\"$content\"}" > /dev/null
    # Delete old file on GitHub
    if [ -n "$sha" ]; then
        gh_curl -X DELETE "$old_api_url" \
            -d "{\"message\":\"Rename $old_name to $new_name\",\"sha\":\"$sha\"}" > /dev/null
    fi
    mv "$NOTES_DIR/$old_name" "$NOTES_DIR/$new_name"
    echo "Renamed '$old_name' to '$new_name'."
    exit 0
}

# --- Parse Flags ---
while getopts "hlg:td:r:" opt; do
    case ${opt} in
        h ) show_help ;;
        l ) list_notes ;;
        g ) search_notes "$OPTARG" ;;
        t ) NOTE_NAME=$(date '+%Y-%m-%d') ;;
        d ) delete_note "$OPTARG" ;;
        r ) rename_note "$OPTARG" "${@:$OPTIND:1}" ;;
        \? ) show_help ;;
    esac
done
shift $((OPTIND -1))

# If -t wasn't passed, get note name from command line arguments
if [ -z "$NOTE_NAME" ]; then
    NOTE_NAME="${1:-index}"
fi

if [[ "$NOTE_NAME" == "gn.conf" ]]; then
    echo "Error: Protection rule triggered. Cannot edit configuration file via gn script loop."
    exit 1
fi

if [[ "$NOTE_NAME" != *.md ]]; then
    NOTE_NAME="${NOTE_NAME}.md"
fi

NOTE_DIR_PATH=$(dirname "$NOTE_NAME")
if [ "$NOTE_DIR_PATH" != "." ]; then
    mkdir -p "$NOTE_DIR_PATH"
fi

# --- Sync From Cloud ---
echo "Fetching latest cloud updates..."
pull_from_github "$NOTE_NAME"

# --- Open the Editor ---
${EDITOR:-nano} "$NOTE_NAME"

# --- Sync Back to GitHub ---
echo "Syncing changes to GitHub..."
push_to_github "$NOTE_NAME"
echo "Sync complete!"

Manual Installation

If you prefer not to use the automated install.sh script, follow these steps to configure gn manually:

1 Create the Config File

The config file lives inside your notes directory as a .conf file. Create the directory first, then the config:

mkdir -p ~/gn
nano ~/gn/gn.conf
chmod 600 ~/gn/gn.conf

Add these three lines, substituting your Github credentials:

GH_TOKEN=ghp_yourpersonalaccesstoken
GH_OWNER=GITHUB-USERNAME
GH_REPO=gn
  • GH_TOKEN - your GitHub personal access token (classic or fine-grained) with repo permissions.
  • GH_OWNER - your explicit GitHub context username profile handle.
  • GH_REPO - the name of the private destination repository (defaults to gn).
2 Make the Script Available Globally

Make the downloaded gn.sh script executable and copy it into your /usr/local/bin) so it's accessible anywhere:

chmod +x gn.sh
sudo cp gn.sh /usr/local/bin/gn
3 Using gn

Run gn from your Terminal. Use flags for advanced features.

gn                  # Opens your main index.md
gn daily-log        # Creates/Opens daily-log.md
gn work/reminders   # Creates work/ directory and opens reminders.md
gn -h               # Displays help page
gn -l               # Lists all your notes
gn -g "todo"        # Finds text matching "todo" inside any note
gn -t               # Opens today's automated scratchpad entry
gn -d daily-log     # Deletes daily-log.md locally and from GitHub
gn -r old new       # Renames old.md to new.md locally and on GitHub

Setting Up a Second Computer

Your notes live in GitHub and sync via the API, so there's nothing to clone. Just create the directory, drop in your config file, and install the script.

1 Create the notes directory and config file
mkdir -p ~/gn
nano ~/gn/gn.conf
chmod 600 ~/gn/gn.conf
GH_TOKEN=ghp_yourpersonalaccesstoken
GH_OWNER=GITHUB-USERNAME
GH_REPO=gn
2 Install gn

Download gn.sh using the button above and install it:

chmod +x gn.sh
sudo cp gn.sh /usr/local/bin/gn

That's it - run gn and it will pull your notes before opening the editor, just like on your first machine.

Editing Notes in the Browser

If you're on a machine without a terminal - or just want a quick edit from any browser - you can use github.dev, a full VS Code instance that runs entirely in the browser and commits directly to your repo.

1 Open your repo in github.dev

Navigate to your gn repo on GitHub, then press . (period) on your keyboard. The page reloads as a VS Code editor with all your notes in the file tree on the left.

Or just swap github.com for github.dev in the URL directly:

https://github.dev/YOUR-USERNAME/gn
2 Edit and commit your note

Open any .md file and make your edits. When done, open the Source Control panel (Ctrl+Shift+G / Cmd+Shift+G), enter a commit message, and click the checkmark. The change is pushed to your repo immediately.

3 Pull before your next terminal session

The next time you run gn on any note, it pulls the latest version from GitHub before opening the editor - no manual sync needed.

Note: gn syncs per-file, so if you edit the same note in github.dev and in the terminal at the same time, the last push wins. Avoid editing the same note simultaneously from two places.