Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
88 changes: 88 additions & 0 deletions agentic-ai/OpenCode/README.md
Original file line number Diff line number Diff line change
@@ -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.
155 changes: 155 additions & 0 deletions agentic-ai/OpenCode/bin/opencode-local
Original file line number Diff line number Diff line change
@@ -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 "$@"
31 changes: 31 additions & 0 deletions agentic-ai/OpenCode/install.sh
Original file line number Diff line number Diff line change
@@ -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'
17 changes: 17 additions & 0 deletions agentic-ai/OpenCode/opencode.json
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
}
}
1 change: 1 addition & 0 deletions linux-desktop/ghostty.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

theme = Dark Pastel
shell-integration = zsh
term = xterm-256color
1 change: 1 addition & 0 deletions macOS/ghostty.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

theme = Dark Pastel
shell-integration = zsh
term = xterm-256color
4 changes: 4 additions & 0 deletions macOS/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions macOS/zshrc.example
Original file line number Diff line number Diff line change
Expand Up @@ -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)


# ═══════════════════════════════════════════════════════════════
Expand Down
Loading