diff --git a/TODO.md b/TODO.md index e031fac..34b18db 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,30 @@ # TODO +## Ghostty config standardization + +The ghostty config lives in two places (`macOS/ghostty.config`, `linux-desktop/ghostty.config`) +with identical content. Consider extracting a universal `cross-platform/ghostty.config` for shared +settings (`term = xterm-256color`, `theme`, `shell-integration`) and keeping OS-specific +configs additive-only (e.g., macOS font settings, Linux-specific tweaks). + +- [ ] Create `cross-platform/ghostty.config` (universal base) +- [ ] Refactor OS configs to extend/override the universal base +- [ ] Update both `setup.sh` scripts to deploy the new layout (universal + OS overlay) + +## OpenCode local models + +Config uses `mlx_lm.server` with Qwen 3.5 9B (4bit, MLX) on the Mac Mini M4. +`opencode-local` script auto-discovers models in `~/.models/`, starts the +server, and launches OpenCode. + +Still to explore: + +- [ ] Test tool-calling quality with Qwen 3.5 9B (does it work well for agentic coding?) +- [ ] Set up on CachyOS/AMD R9700 with Gemma 4 and Qwen 3.6 (via llama.cpp or lemonade) +- [ ] Add CachyOS provider config once the model/runtime is chosen +- [ ] Consider `small_model` for lightweight tasks (title gen, etc.) +- [ ] Install `opencode-local` via install.sh and verify PATH + ## macOS zshrc modernization Port the linux-desktop zsh enhancements to the macOS config. See diff --git a/agentic-ai/OpenCode/README.md b/agentic-ai/OpenCode/README.md new file mode 100644 index 0000000..7483dc5 --- /dev/null +++ b/agentic-ai/OpenCode/README.md @@ -0,0 +1,88 @@ +# OpenCode Config + +Version-controlled source of truth for `~/.config/opencode/opencode.json`. +Running `install.sh` wires everything up via symlinks so changes here take +effect immediately. + +## Activation + +```bash +bash agentic-ai/OpenCode/install.sh +``` + +This will: +- Back up your existing `~/.config/opencode/opencode.json` (if not already a symlink) +- Symlink `~/.config/opencode/opencode.json` → this `opencode.json` +- Symlink `bin/opencode-local` → `~/.local/bin/opencode-local` + +Restart OpenCode after running. + +## Usage + +### Launch with model picker + +```bash +opencode-local +``` + +This scans `~/.models/` for MLX model directories, lets you pick one, starts +the server, waits for it to be ready, then launches OpenCode. The server +shuts down automatically when you exit OpenCode. + +Pass through additional OpenCode flags: + +```bash +opencode-local --model anthropic/claude-sonnet-4-6 +``` + +### Launch manually + +```bash +mlx_lm.server --model ~/.models/mlx-community--Qwen3.5-9B-4bit & +opencode +# kill %1 when done +``` + +### Model selection in OpenCode + +Once connected to a server, select the model via: + +``` +/models +``` + +Pick `mlx/mlx-community/Qwen3.5-9B-4bit` (or whichever model the server has +loaded). + +## Recommended models + +| Machine | Model | Notes | +|---------|-------|-------| +| Mac Mini M4 | **Qwen 3.5 9B (4bit)** — `~/.models/mlx-community--Qwen3.5-9B-4bit` | MLX-optimized, already on this machine | +| CachyOS / AMD R9700 AI PRO | **Gemma 4** / **Qwen 3.6** (larger variants) | Powerful AMD GPU — test these on the desktop | + +## Usage alongside Claude + +OpenCode supplements Claude Code for: +- **Small, fast edits** that don't need Claude's full context +- **Exploratory work** on unfamiliar code +- **Offline-capable tasks** when you're not connected +- **Private/air-gapped work** where code shouldn't leave the machine + +Claude Code handles complex multi-file refactors, architecture decisions, +and heavy debugging. + +## What this configures + +### MLX provider + +Registers `mlx_lm.server` (OpenAI-compatible endpoint at +`http://127.0.0.1:8080/v1`) as a provider with pre-configured model entries. +Add or remove models by editing `opencode.json` directly — the symlink means +changes are live immediately after restarting OpenCode. + +### `opencode-local` command + +A wrapper script that scans `~/.models/` for MLX model directories, presents +a picker, starts the server, and launches OpenCode. On exit, the server is +cleaned up automatically. diff --git a/agentic-ai/OpenCode/bin/opencode-local b/agentic-ai/OpenCode/bin/opencode-local new file mode 100755 index 0000000..1f9e895 --- /dev/null +++ b/agentic-ai/OpenCode/bin/opencode-local @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# Launch OpenCode with a local MLX model. +# Scans ~/.models/ for MLX model dirs, lets you pick one, starts the server, +# waits for it to be ready, then launches OpenCode. Cleans up on exit. +# +# Usage: opencode-local [opencode-args...] +# opencode-local --log # tail the server log + +set -euo pipefail + +MODELS_DIR="${MLX_MODELS_DIR:-$HOME/.models}" +SERVER_HOST="${MLX_HOST:-127.0.0.1}" +SERVER_PORT="${MLX_PORT:-8080}" +SERVER_URL="http://${SERVER_HOST}:${SERVER_PORT}" +LOG_DIR="${XDG_STATE_HOME:-$HOME/.local/share}/opencode-local" +LOG_FILE="$LOG_DIR/server.log" + +mkdir -p "$LOG_DIR" + +# Log a timestamped message to the log file +log() { + printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE" +} + +# ── discover models ────────────────────────────────────────────────────── + +discover_models() { + local entries=() + for dir in "$MODELS_DIR"/mlx-community--*; do + [[ -d "$dir" ]] || continue + local name="${dir##*/}" # mlx-community--Qwen3.5-9B-4bit + local display="${name#mlx-community--}" # Qwen3.5-9B-4bit + # Pretty-print: replace dashes with spaces, wrap trailing bit in parens + display="${display//-/ }" + # Extract trailing "Nbit" and wrap in parens (bash 3.2 compat) + local bits="${display##* }" + display="${display% $bits}" + display="$display ($bits)" + entries+=("$dir|$display") + done + printf '%s\n' "${entries[@]}" +} + +# ── model picker ────────────────────────────────────────────────────────── + +pick_model() { + local -a models=() + while IFS= read -r line; do + models+=("$line") + done < <(discover_models | sort -t'|' -k2) + + if [[ ${#models[@]} -eq 0 ]]; then + printf 'No MLX models found in %s\n' "$MODELS_DIR" >&2 + printf 'Pull one first:\n' >&2 + printf ' huggingface-cli download mlx-community/Qwen3.5-9B-4bit --local-dir ~/.models/mlx-community--Qwen3.5-9B-4bit\n' >&2 + exit 1 + fi + + if [[ ${#models[@]} -eq 1 ]]; then + # Only one model — use it without prompting + IFS='|' read -r selected_path selected_name <<< "${models[0]}" + printf 'Using model: %s\n' "$selected_name" >&2 + printf '%s\n' "$selected_path" + return + fi + + printf '\nAvailable models:\n' >&2 + for i in "${!models[@]}"; do + IFS='|' read -r _ name <<< "${models[$i]}" + printf ' [%d] %s\n' $((i + 1)) "$name" >&2 + done + printf '\nSelect model [1-%d]: ' "${#models[@]}" >&2 + + local choice + read -r choice + if [[ ! "$choice" =~ ^[0-9][0-9]*$ ]] || (( choice < 1 || choice > ${#models[@]} )); then + printf 'Invalid selection\n' >&2 + exit 1 + fi + + IFS='|' read -r selected_path selected_name <<< "${models[$((choice - 1))]}" + printf 'Using model: %s\n' "$selected_name" >&2 + printf '%s\n' "$selected_path" +} + +# ── wait for server ─────────────────────────────────────────────────────── + +wait_for_server() { + local max_attempts=60 # 30 seconds + local attempt=0 + while ! curl -sf "$SERVER_URL/v1/models" >/dev/null 2>&1; do + attempt=$((attempt + 1)) + if (( attempt > max_attempts )); then + log "Server failed to start within $((max_attempts / 2)) seconds" + printf '\nServer failed to start within %d seconds\n' "$((max_attempts / 2))" >&2 + printf 'Check the log for details: %s\n' "$LOG_FILE" >&2 + exit 1 + fi + printf '\rWaiting for model server to be ready... (%ds)' "$((attempt / 2))" + sleep 0.5 + done + printf '\rModel server ready%s\n' ' ' +} + +# ── main ────────────────────────────────────────────────────────────────── + +# --log flag: just tail the log file +if [[ "${1:-}" == "--log" ]]; then + if [[ -f "$LOG_FILE" ]]; then + tail -f "$LOG_FILE" + else + printf 'No log file yet at: %s\n' "$LOG_FILE" >&2 + exit 1 + fi + exit 0 +fi + +MODEL_PATH="$(pick_model)" +log "Selected model: $MODEL_PATH" +log "Starting MLX server (host=$SERVER_HOST port=$SERVER_PORT)" + +printf 'Starting MLX server with: %s\n' "$MODEL_PATH" + +# Start the server in the background, tee stderr to log +mlx_lm.server \ + --model "$MODEL_PATH" \ + --host "$SERVER_HOST" \ + --port "$SERVER_PORT" \ + --max-tokens 4096 \ + --prompt-cache-bytes 4294967296 \ + --log-level ERROR \ + 2>>"$LOG_FILE" & + +SERVER_PID=$! +log "Server PID: $SERVER_PID" + +# Cleanup on exit +cleanup() { + local exit_code=$? + if [[ $exit_code -ne 0 ]]; then + log "Exited with code $exit_code — see log: $LOG_FILE" + fi + log "Shutting down server (PID $SERVER_PID)" + printf '\nShutting down model server...\n' + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + log "Server stopped" +} +trap cleanup EXIT INT TERM + +wait_for_server +log "Server ready" + +# Launch OpenCode — pass through any extra args +opencode "$@" diff --git a/agentic-ai/OpenCode/install.sh b/agentic-ai/OpenCode/install.sh new file mode 100755 index 0000000..dd2e8d2 --- /dev/null +++ b/agentic-ai/OpenCode/install.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Idempotent setup: symlinks this repo's OpenCode config into ~/.config/opencode/ +# Safe to re-run. Backs up any existing opencode.json before replacing it. + +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OPENCODE_DIR="$HOME/.config/opencode" +CONFIG="$OPENCODE_DIR/opencode.json" + +printf 'Installing from: %s\n' "$REPO_DIR" + +mkdir -p "$OPENCODE_DIR" + +# Back up opencode.json if it exists and is not already one of ours +if [[ -e "$CONFIG" && ! -L "$CONFIG" ]]; then + BACKUP="$CONFIG.bak.$(date +%Y%m%d%H%M%S)" + printf 'Backing up existing opencode.json → %s\n' "$BACKUP" + mv "$CONFIG" "$BACKUP" +fi + +ln -sf "$REPO_DIR/opencode.json" "$CONFIG" +printf 'Linked: opencode.json\n' + +# Symlink launch script into PATH +LOCAL_BIN="$HOME/.local/bin" +mkdir -p "$LOCAL_BIN" +ln -sf "$REPO_DIR/bin/opencode-local" "$LOCAL_BIN/opencode-local" +printf 'Linked: bin/opencode-local → %s/opencode-local\n' "$LOCAL_BIN" + +printf '\nDone. Restart OpenCode for changes to take effect.\n' diff --git a/agentic-ai/OpenCode/opencode.json b/agentic-ai/OpenCode/opencode.json new file mode 100644 index 0000000..8cb8ead --- /dev/null +++ b/agentic-ai/OpenCode/opencode.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://opencode.ai/config.json", + "provider": { + "mlx": { + "npm": "@ai-sdk/openai-compatible", + "name": "MLX (local)", + "options": { + "baseURL": "http://127.0.0.1:8080/v1" + }, + "models": { + "mlx-community/Qwen3.5-9B-4bit": { + "name": "Qwen 3.5 9B (4bit, MLX)" + } + } + } + } +} diff --git a/linux-desktop/ghostty.config b/linux-desktop/ghostty.config index 343919f..a60cb99 100644 --- a/linux-desktop/ghostty.config +++ b/linux-desktop/ghostty.config @@ -2,3 +2,4 @@ theme = Dark Pastel shell-integration = zsh +term = xterm-256color diff --git a/macOS/ghostty.config b/macOS/ghostty.config index 343919f..a60cb99 100644 --- a/macOS/ghostty.config +++ b/macOS/ghostty.config @@ -2,3 +2,4 @@ theme = Dark Pastel shell-integration = zsh +term = xterm-256color diff --git a/macOS/setup.sh b/macOS/setup.sh index 42151aa..cc19774 100755 --- a/macOS/setup.sh +++ b/macOS/setup.sh @@ -229,6 +229,10 @@ else fi fi +# ── Git: GPG signing ───────────────────────────────────────────────────────── +run git config --global gpg.program /opt/homebrew/bin/gpg +add_to_zshrc 'export GPG_TTY=$(tty)' + # ── Medium-priority pipx packages ───────────────────────────────────────────── echo "==> Installing pipx packages..." if [[ "$DRY_RUN" == false ]]; then diff --git a/macOS/zshrc.example b/macOS/zshrc.example index 6027838..dcd9163 100644 --- a/macOS/zshrc.example +++ b/macOS/zshrc.example @@ -98,6 +98,7 @@ alias gl='git log --oneline --graph --decorate' export EDITOR='micro' export LANG='en_US.UTF-8' export LC_ALL='en_US.UTF-8' +export GPG_TTY=$(tty) # ═══════════════════════════════════════════════════════════════