Created
April 2, 2025 03:13
-
-
Save androidStern/1f7e9ffa62a4b1db357fe2b6fb56b8a9 to your computer and use it in GitHub Desktop.
CTX - A tool to manage named lists of files in your repo as 'contexts' for quick pasting into LLM chats. Use it to create a list of auth related files, for example and easily copy their contents with CTX later. Inspired by YacineMTB's context script.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # ctx - Manage file/directory context lists for LLM usage, then pipe combined content to clipboard. | |
| # | |
| # Dependencies: | |
| # - fzf, fd, bat, pbcopy (on macOS) or an equivalent on other systems. | |
| # - highlight (optional) for nicer previews, else we fallback to bat with --style=plain. | |
| # | |
| # Usage: | |
| # ctx -n <context_name> Create or overwrite a context by selecting multiple files/folders in fzf. | |
| # ctx -p <context_name> Read .llm_context/<context_name>, dump contents, copy to clipboard. | |
| # ctx -p Fuzzy-select from .llm_context/* with a preview, then copy result to clipboard. | |
| # ctx -e Edit a context. Opens in default editor. | |
| # ctx -d Fuzzy-select a context to delete (with confirmation). | |
| # ------------------------- Configuration ------------------------- | |
| # Attempt to locate the project's root via Git; if not in a Git repo, fallback to current dir. | |
| if command -v git &>/dev/null && git rev-parse --show-toplevel &>/dev/null; then | |
| PROJECT_ROOT="$(git rev-parse --show-toplevel)" | |
| else | |
| PROJECT_ROOT="$(pwd)" | |
| fi | |
| CONTEXT_DIR="$PROJECT_ROOT/.llm_context" | |
| # ------------------------- Helpers ----------------------- | |
| die() { | |
| echo "Error: $*" >&2 | |
| exit 1 | |
| } | |
| check_dependencies() { | |
| for cmd in fzf fd bat pbcopy; do | |
| command -v "$cmd" &>/dev/null || die "Missing dependency: $cmd" | |
| done | |
| } | |
| add_to_gitignore() { | |
| local ignore_file="$PROJECT_ROOT/.gitignore" | |
| local target_entry=".llm_context" | |
| # If there's no .gitignore, do nothing. | |
| if [[ ! -f "$ignore_file" ]]; then | |
| # echo "No .gitignore found in $PROJECT_ROOT; skipping ignore update." | |
| return 0 | |
| fi | |
| # Check if .llm_context is already ignored | |
| if grep -qxF "$target_entry" "$ignore_file"; then | |
| # echo ".llm_context is already in .gitignore; nothing to do." | |
| return 0 | |
| fi | |
| # Otherwise, append it | |
| echo "$target_entry" >>"$ignore_file" | |
| echo "Added '$target_entry' to .gitignore." | |
| } | |
| init_context_dir() { | |
| [ -d "$CONTEXT_DIR" ] || mkdir -p "$CONTEXT_DIR" | |
| add_to_gitignore | |
| } | |
| show_banner() { | |
| local gist_url="https://gist.githubusercontent.com/androidStern/d9a66190784921b5d86552ae63441286/raw/67b543455ab09a5cf250cdc5b9802f9e707e9816/ctx_logo.txt" | |
| # Try to fetch the banner; if curl fails or is empty, we do nothing. | |
| local banner | |
| banner=$(curl --fail -s "$gist_url" 2>/dev/null) || return 0 | |
| [[ -z "$banner" ]] && return 0 | |
| # "Auto-scroll" line-by-line | |
| while IFS= read -r line; do | |
| echo "$line" | |
| sleep 0.03 # adjust speed as desired | |
| done <<<"$banner" | |
| } | |
| show_help() { | |
| show_banner | |
| cat <<EOF | |
| NAME | |
| ctx - Manage lists of files/directores as named "contexts" for LLM usage. Easily copy all file contents in a context to your clipboard. | |
| SYNOPSIS | |
| ctx [-n | new] <context_name> | |
| ctx [-p | copy] [<context_name>] | |
| ctx [-e | edit] [<context_name>] | |
| ctx [-d | del] [<context_name>] | |
| ctx [-h | --help | help] | |
| DESCRIPTION | |
| ctx lets you quickly group files (or folders) into named "contexts" using a fuzzy finder. Later, it can instantly dump | |
| all the grouped files' contents directly to your clipboard — ideal for pasting into LLM prompts. | |
| Easily edit or delete contexts anytime. | |
| COMMANDS | |
| -n, new <context_name> | |
| Create or overwrite a context by picking multiple files/folders in fzf. | |
| If <context_name> already exists, it will be overwritten. | |
| -p, copy [<context_name>] | |
| Copy the specified context to your clipboard. If <context_name> is omitted, | |
| fuzzy-select an existing context. | |
| -e, edit [<context_name>] | |
| Open the specified context file in your \$EDITOR. If omitted, fuzzy-select first. | |
| -d, del [<context_name>] | |
| Delete the specified context. If omitted, fuzzy-select from existing contexts. | |
| Confirmation is required before deletion. | |
| -h, --help, help | |
| Show this help message. | |
| EXAMPLES | |
| ctx -n myContext # or ctx new myContext | |
| ctx -p myContext # or ctx copy myContext | |
| ctx -e # or ctx edit (then pick a context) | |
| ctx -d oldStuff # or ctx del oldStuff | |
| EOF | |
| } | |
| # ------------------------- "mfat" multi-file-formated-cat ------------------ | |
| mfat() { | |
| echo "- The following content is a combination of docs, code, and PRDs related to my application." | |
| echo "- Each section is delineated by 'File: /path/to/file'." | |
| echo | |
| while IFS="" read -r selection; do | |
| # Skip blank lines | |
| [[ -z "$selection" ]] && continue | |
| echo "-------------------------------------------------------------------" | |
| echo "File: $selection" | |
| if [[ -d "$selection" ]]; then | |
| fd --type file . "$selection" | | |
| xargs -I{} bat --decorations=always --style=header "{}" | |
| elif [[ -f "$selection" ]]; then | |
| bat --decorations=always --style=header "$selection" | |
| else | |
| echo "Skipping: '$selection' (not a file or directory)" | |
| fi | |
| echo | |
| done | |
| } | |
| # ------------------------- Core Functions ------------------------- | |
| create_context() { | |
| local ctx_name="$1" | |
| local ctx_file="$CONTEXT_DIR/$ctx_name" | |
| echo "Select files/directories with fzf (press TAB to mark multiple, Enter to confirm):" | |
| echo "-----------------------------------------------------------------------" | |
| fd --type file --type directory --exclude "node_modules" . | | |
| sort | | |
| fzf --multi \ | |
| --preview 'if [ -d {} ]; then tree -C -L 1 {} | head -50; else bat --color=always --style=header {} | head -50; fi' \ | |
| --bind "tab:toggle,shift-tab:toggle+up,ctrl-space:toggle-preview" \ | |
| --header 'Tab: select | Ctrl-Space: toggle preview' \ | |
| >"$ctx_file" | |
| echo "Created/updated context list -> $ctx_file" | |
| } | |
| copy_context() { | |
| local ctx_name="$1" | |
| local ctx_file="$CONTEXT_DIR/$ctx_name" | |
| if [ ! -f "$ctx_file" ]; then | |
| die "Context file not found: $ctx_file" | |
| fi | |
| mfat <"$ctx_file" | pbcopy | |
| echo "Context '$ctx_name' content copied to clipboard." | |
| } | |
| fzf_pick_context_and_copy() { | |
| cd "$CONTEXT_DIR" || die "Failed to cd into $CONTEXT_DIR" | |
| # List context files ignoring hidden | |
| local contexts | |
| contexts=$(find . -maxdepth 1 -type f -not -path "*/\.*" | sort) | |
| if [ -z "$contexts" ]; then | |
| echo "No contexts found in '$CONTEXT_DIR'." | |
| cd - &>/dev/null | |
| return 1 | |
| fi | |
| local selected | |
| selected=$(echo "$contexts" | sed 's|^\./||' | fzf \ | |
| --height 40% \ | |
| --layout=reverse \ | |
| --border \ | |
| --prompt="Select context: " \ | |
| --preview="(highlight -O ansi -s monokai --force --syntax-by-name=md $CONTEXT_DIR/{} 2>/dev/null || \ | |
| bat --color=always --style=plain --language=markdown $CONTEXT_DIR/{})" \ | |
| --preview-window="right:50%") | |
| if [ -n "$selected" ]; then | |
| cd - &>/dev/null | |
| copy_context "$selected" | |
| else | |
| cd - &>/dev/null | |
| echo "No context selected." | |
| fi | |
| } | |
| delete_context() { | |
| cd "$CONTEXT_DIR" || die "Failed to cd into $CONTEXT_DIR" | |
| local contexts | |
| contexts=$(find . -maxdepth 1 -type f -not -path "*/\.*" | sort) | |
| if [ -z "$contexts" ]; then | |
| echo "No contexts found in '$CONTEXT_DIR'." | |
| cd - &>/dev/null | |
| return 1 | |
| fi | |
| local selected | |
| selected=$(echo "$contexts" | sed 's|^\./||' | fzf \ | |
| --height 40% \ | |
| --layout=reverse \ | |
| --border \ | |
| --prompt="Select context to delete: " \ | |
| --preview="(highlight -O ansi -s monokai --force --syntax-by-name=md $CONTEXT_DIR/{} 2>/dev/null || \ | |
| bat --color=always --style=plain --language=markdown $CONTEXT_DIR/{})" \ | |
| --preview-window="right:50%") | |
| cd - &>/dev/null | |
| if [ -n "$selected" ]; then | |
| echo "Deleting '$selected'. Press 'y' to confirm, or CTRL-C to abort." | |
| rm -i "$CONTEXT_DIR/$selected" | |
| else | |
| echo "No context selected." | |
| fi | |
| } | |
| edit_context() { | |
| local ctx_name="$1" | |
| # Use $EDITOR if defined, otherwise fallback to nano | |
| local editor="${EDITOR:-nano}" | |
| # If user specified a context name, open that file directly | |
| if [[ -n "$ctx_name" ]]; then | |
| local ctx_file="$CONTEXT_DIR/$ctx_name" | |
| if [[ ! -f "$ctx_file" ]]; then | |
| echo "Context '$ctx_name' does not exist: $ctx_file" | |
| return 1 | |
| fi | |
| "$editor" "$ctx_file" | |
| return 0 | |
| fi | |
| # Otherwise, fuzzy-pick a context | |
| cd "$CONTEXT_DIR" || { | |
| echo "Failed to enter $CONTEXT_DIR" | |
| return 1 | |
| } | |
| local contexts | |
| contexts=$(find . -maxdepth 1 -type f -not -path "*/\.*" | sort) | |
| if [[ -z "$contexts" ]]; then | |
| echo "No contexts found in '$CONTEXT_DIR'." | |
| cd - &>/dev/null | |
| return 1 | |
| fi | |
| local selected | |
| selected=$(echo "$contexts" | sed 's|^\./||' | fzf \ | |
| --height 40% \ | |
| --layout=reverse \ | |
| --border \ | |
| --prompt="Select context to edit: " \ | |
| --preview="(highlight -O ansi -s monokai --force --syntax-by-name=md $CONTEXT_DIR/{} 2>/dev/null || \ | |
| bat --color=always --style=plain --language=markdown $CONTEXT_DIR/{})" \ | |
| --preview-window="right:50%") | |
| cd - &>/dev/null | |
| if [[ -n "$selected" ]]; then | |
| "$editor" "$CONTEXT_DIR/$selected" | |
| else | |
| echo "No context selected." | |
| fi | |
| } | |
| # ------------------------- Main Entry Point ----------------------- | |
| main() { | |
| check_dependencies | |
| init_context_dir | |
| # If no args, show help | |
| if [ $# -lt 1 ]; then | |
| show_help | |
| exit 0 | |
| fi | |
| local cmd="$1" | |
| shift # Next argument may be <context_name> | |
| case "$cmd" in | |
| # --------------- Short Flags --------------- | |
| -n) | |
| # ctx -n <context_name> | |
| if [[ -z "$1" ]]; then | |
| die "Please specify a context name after '-n'." | |
| fi | |
| create_context "$1" | |
| ;; | |
| -p) | |
| # ctx -p [context_name?] | |
| if [[ -z "$1" ]]; then | |
| fzf_pick_context_and_copy | |
| else | |
| copy_context "$1" | |
| fi | |
| ;; | |
| -e) | |
| # ctx -e [context_name?] | |
| if [[ -z "$1" ]]; then | |
| edit_context | |
| else | |
| edit_context "$1" | |
| fi | |
| ;; | |
| -d) | |
| # ctx -d [context_name?] | |
| if [[ -z "$1" ]]; then | |
| delete_context | |
| else | |
| # Immediately delete if file exists | |
| local ctx_file="$CONTEXT_DIR/$1" | |
| if [[ ! -f "$ctx_file" ]]; then | |
| echo "Context '$1' does not exist." | |
| exit 1 | |
| fi | |
| echo "Deleting '$1'. Press 'y' to confirm, or CTRL-C to abort." | |
| rm -i "$ctx_file" | |
| fi | |
| ;; | |
| -h | --help) | |
| show_help | |
| ;; | |
| # --------------- Verbose Subcommands --------------- | |
| help) | |
| show_help | |
| ;; | |
| new) | |
| # ctx new <context_name> | |
| if [[ -z "$1" ]]; then | |
| die "Please specify a context name: ctx new <name>" | |
| fi | |
| create_context "$1" | |
| ;; | |
| copy) | |
| # ctx copy [context_name?] | |
| if [[ -z "$1" ]]; then | |
| fzf_pick_context_and_copy | |
| else | |
| copy_context "$1" | |
| fi | |
| ;; | |
| edit) | |
| # ctx edit [context_name?] | |
| if [[ -z "$1" ]]; then | |
| edit_context | |
| else | |
| edit_context "$1" | |
| fi | |
| ;; | |
| del) | |
| # ctx del [context_name?] | |
| if [[ -z "$1" ]]; then | |
| delete_context | |
| else | |
| local ctx_file="$CONTEXT_DIR/$1" | |
| if [[ ! -f "$ctx_file" ]]; then | |
| echo "Context '$1' does not exist." | |
| exit 1 | |
| fi | |
| echo "Deleting '$1'. Press 'y' to confirm, or CTRL-C to abort." | |
| rm -i "$ctx_file" | |
| fi | |
| ;; | |
| *) | |
| echo "Unrecognized command or flag: $cmd" | |
| show_help | |
| exit 1 | |
| ;; | |
| esac | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment