From d754bae5ad2228658836818ad427889673a34d85 Mon Sep 17 00:00:00 2001 From: Prad Nukala Date: Fri, 16 Oct 2026 14:06:23 -0400 Subject: [PATCH] feat: set opencode as default --- README.md | 18 +++--- crush-provider/crush.json | 18 ------ gh-commit.tar.gz | Bin 12304 -> 12621 bytes smartcommit.py | 125 +++++++++++++++++++++++++------------- 4 files changed, 93 insertions(+), 68 deletions(-) delete mode 100644 crush-provider/crush.json diff --git a/README.md b/README.md index 3e16333..aa2ea17 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # gh-commit AI-powered scoped git commits as a GitHub CLI extension, driven by the -[Crush](https://github.com/charmbracelet/crush) CLI. Scopes map Conventional +[OpenCode](https://opencode.ai) CLI. Scopes map Conventional Commit scope names to path prefixes. They are generated from the repository file tree, stored in a local DuckDB database, and auto-regenerate whenever `.gitignore` changes. Changed files are grouped by scope and committed one @@ -16,10 +16,12 @@ gh extension install prdlk/gh-commit ### Requirements - [uv](https://docs.astral.sh/uv) — Python package runner (handles dependencies automatically) -- [Crush](https://github.com/charmbracelet/crush) — non-interactive AI runner -- `GROQ_API_KEY` — authenticates the default Groq model +- [OpenCode](https://opencode.ai) — non-interactive AI runner (`opencode run`) +- OpenCode Go access for the default model — `opencode auth login` (choose OpenCode Go) or set `OPENCODE_API_KEY` -By default, gh-commit uses Groq's `openai/gpt-oss-120b` via the bundled `crush-provider` config. Override the model with `GH_COMMIT_CRUSH_MODEL`. +By default, gh-commit uses `opencode-go/gpt-5.6-luna`: the fastest model in OpenCode Go's largest context tier (1.05M tokens). Override it with `GH_COMMIT_OPENCODE_MODEL` (any `provider/model` from `opencode models`). + +Prompts run through a private `opencode run --standalone` server with an inline read-only `gh-commit` agent: every tool except `read`, `glob`, and `grep` is denied, so the model never modifies the working tree. ## Usage @@ -34,7 +36,7 @@ gh commit # commit changes grouped by scope | Command | Behavior | |---|---| | `gh commit` | Full scoped-commit flow: group changed files by scope, generate a message per group, confirm, commit, offer to push | -| `gh commit init` | Generate scopes from the file tree via Crush; migrates legacy configs; confirms overwrite | +| `gh commit init` | Generate scopes from the file tree via OpenCode; migrates legacy configs; confirms overwrite | | `gh commit refresh` | Show current scopes, regenerate with existing scopes as context, confirm apply | | `gh commit sync` | Create/update GitHub labels from scopes (color = first 6 hex chars of MD5 of the scope name) | | `gh commit list` | Table of configured repositories: name, path, scope count | @@ -52,9 +54,9 @@ Flags: `--auto` (skip confirmations), `--push` (auto-push). | `GH_COMMIT_AUTO=1` | Skip all confirmation prompts | | `GH_COMMIT_PUSH=1` | Auto-push after commits | | `GH_COMMIT_NO_AUTO_REFRESH=1` | Don't auto-regenerate scopes when `.gitignore` changes | -| `GH_COMMIT_CRUSH_CMD` | Override the Crush command (default `crush`) | -| `GH_COMMIT_CRUSH_MODEL` | Override the Crush model (default `groq/openai/gpt-oss-120b`) | -| `GH_COMMIT_CRUSH_TIMEOUT` | Per-prompt timeout in seconds (default `120`) | +| `GH_COMMIT_OPENCODE_CMD` | Override the OpenCode command (default `opencode`) | +| `GH_COMMIT_OPENCODE_MODEL` | Override the model (default `opencode-go/gpt-5.6-luna`) | +| `GH_COMMIT_OPENCODE_TIMEOUT` | Per-prompt timeout in seconds (default `120`) | | `GH_COMMIT_DEBUG=1` | Show scope-response parse diagnostics | ## Storage diff --git a/crush-provider/crush.json b/crush-provider/crush.json deleted file mode 100644 index 67a528a..0000000 --- a/crush-provider/crush.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "$schema": "https://charm.land/crush.json", - "providers": { - "groq": { - "type": "openai-compat", - "base_url": "https://api.groq.com/openai/v1", - "api_key": "$GROQ_API_KEY", - "models": [ - { - "id": "openai/gpt-oss-120b", - "name": "GPT-OSS 120B on Groq", - "context_window": 131072, - "default_max_tokens": 8192 - } - ] - } - } -} diff --git a/gh-commit.tar.gz b/gh-commit.tar.gz index 006530906af7fad9552035bf238a807213bf5444..2ad1d0f0ba79592ea0b16c93205bc0c10fa0556c 100644 GIT binary patch literal 12621 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 literal 12304 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 diff --git a/smartcommit.py b/smartcommit.py index c3ac3aa..503b20c 100644 --- a/smartcommit.py +++ b/smartcommit.py @@ -9,7 +9,7 @@ # /// """gh-commit - AI-powered scoped git commits. -Both AI steps run through `crush run`: scope generation sends the repository's +Both AI steps run through `opencode run`: scope generation sends the repository's file tree with embedded scope-identification instructions, and commit-message writing sends the staged diff with embedded Conventional Commits instructions. Scopes live in a local DuckDB and auto-regenerate @@ -41,20 +41,28 @@ console = Console() DB_DIR = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share")) / "gh-commit" DB_PATH = DB_DIR / "gh-commit.db" -# Crush handles both AI steps in non-interactive mode. The instructions formerly -# stored as mods roles are embedded below so this tool is self-contained. -CRUSH_CMD = os.environ.get("GH_COMMIT_CRUSH_CMD", "crush").split() -DEFAULT_CRUSH_MODEL = "groq/openai/gpt-oss-120b" -CRUSH_MODEL = os.environ.get("GH_COMMIT_CRUSH_MODEL", DEFAULT_CRUSH_MODEL) -CRUSH_CONFIG_DIR = Path(__file__).with_name("crush-provider") -CRUSH_TIMEOUT = int(os.environ.get("GH_COMMIT_CRUSH_TIMEOUT", "120")) +# OpenCode handles both AI steps in non-interactive mode. The instructions +# formerly stored as mods roles are embedded below so this tool is self-contained. +OPENCODE_CMD = os.environ.get("GH_COMMIT_OPENCODE_CMD", "opencode").split() +# Fastest model in the largest-context (1.05M) tier of the OpenCode Go provider. +DEFAULT_OPENCODE_MODEL = "opencode-go/gpt-5.6-luna" +OPENCODE_MODEL = os.environ.get("GH_COMMIT_OPENCODE_MODEL", DEFAULT_OPENCODE_MODEL) +OPENCODE_TIMEOUT = int(os.environ.get("GH_COMMIT_OPENCODE_TIMEOUT", "120")) +# Read-only agent: everything is denied except read/glob/grep, so the model can +# never modify the working tree. (Denying every tool breaks OpenCode's free tier.) +OPENCODE_AGENT = "gh-commit" +OPENCODE_AGENT_CONFIG = { + "mode": "primary", + "description": "gh-commit message and scope generator (read-only)", + "permission": {"*": "deny", "read": "allow", "glob": "allow", "grep": "allow"}, +} AUTO_CONFIRM = os.environ.get("GH_COMMIT_AUTO", "0") == "1" AUTO_PUSH = os.environ.get("GH_COMMIT_PUSH", "0") == "1" NO_AUTO_REFRESH = os.environ.get("GH_COMMIT_NO_AUTO_REFRESH", "0") == "1" DEBUG = os.environ.get("GH_COMMIT_DEBUG", "0") == "1" -VERSION = "5.0.1" +VERSION = "6.0.0" # ── Helpers ─────────────────────────────────────────────────────────────────── @@ -98,10 +106,10 @@ def require_git() -> Path: return get_repo_root() -# ── Crush CLI client ────────────────────────────────────────────────────────── +# ── OpenCode CLI client ─────────────────────────────────────────────────────── -class CrushError(RuntimeError): - """Raised when `crush run` fails to produce output.""" +class OpencodeError(RuntimeError): + """Raised when `opencode run` fails to produce output.""" def build_filetree(repo_path: Path) -> str: @@ -112,29 +120,62 @@ def build_filetree(repo_path: Path) -> str: return "\n".join(files) -def crush_prompt(text: str, cwd: Path) -> str: - """Send `text` to `crush run` and return its reply.""" - cmd = [*CRUSH_CMD, "run", "--quiet"] - if CRUSH_MODEL: - cmd += ["--model", CRUSH_MODEL] +def opencode_env() -> dict[str, str]: + """Environment with the text-only gh-commit agent merged into inline config.""" env = os.environ.copy() - if CRUSH_MODEL == DEFAULT_CRUSH_MODEL: - env.setdefault("CRUSH_GLOBAL_CONFIG", str(CRUSH_CONFIG_DIR)) + try: + config = json.loads(env.get("OPENCODE_CONFIG_CONTENT") or "{}") + except json.JSONDecodeError: + config = {} + config.setdefault("agent", {})[OPENCODE_AGENT] = OPENCODE_AGENT_CONFIG + env["OPENCODE_CONFIG_CONTENT"] = json.dumps(config) + return env + + +def parse_opencode_events(stdout: str) -> str: + """Return the final assistant message text from `opencode run --format json`.""" + texts: dict[str, list[str]] = {} + last_message = None + for line in stdout.splitlines(): + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + if event.get("type") == "error": + error = event.get("error") or {} + raise OpencodeError(error.get("message") or json.dumps(error)) + part = event.get("part") or {} + if event.get("type") == "text" and part.get("text"): + last_message = part.get("messageID") + texts.setdefault(last_message, []).append(part["text"]) + return "".join(texts.get(last_message, [])).strip() + + +def opencode_prompt(text: str, cwd: Path) -> str: + """Send `text` to `opencode run` and return its reply.""" + # --standalone runs a private server so this process's env (API keys, inline + # agent config) and cwd apply, instead of the shared background service's. + cmd = [*OPENCODE_CMD, "run", "--standalone", "--format", "json", "--agent", OPENCODE_AGENT] + if OPENCODE_MODEL: + cmd += ["--model", OPENCODE_MODEL] try: result = subprocess.run( - cmd, input=text, capture_output=True, text=True, env=env, - cwd=str(cwd), timeout=CRUSH_TIMEOUT, + cmd, input=text, capture_output=True, text=True, env=opencode_env(), + cwd=str(cwd), timeout=OPENCODE_TIMEOUT, ) except FileNotFoundError as e: - raise CrushError( - f"Could not run the Crush CLI ({CRUSH_CMD[0]!r} not found). " - "Install Crush, or set GH_COMMIT_CRUSH_CMD to a working command." + raise OpencodeError( + f"Could not run the OpenCode CLI ({OPENCODE_CMD[0]!r} not found). " + "Install OpenCode, or set GH_COMMIT_OPENCODE_CMD to a working command." ) from e except subprocess.TimeoutExpired as e: - raise CrushError(f"crush run timed out after {CRUSH_TIMEOUT}s") from e + raise OpencodeError(f"opencode run timed out after {OPENCODE_TIMEOUT}s") from e + reply = parse_opencode_events(result.stdout) if result.returncode != 0: - raise CrushError(result.stderr.strip() or f"crush run exited with {result.returncode}") - return result.stdout.strip() + raise OpencodeError(result.stderr.strip() or f"opencode run exited with {result.returncode}") + if not reply: + raise OpencodeError("opencode run returned no text") + return reply # ── Database ────────────────────────────────────────────────────────────────── @@ -417,7 +458,7 @@ def filter_diff(diff: str) -> str: return "\n".join(lines) -# ── AI integration (Crush CLI) ──────────────────────────────────────────────── +# ── AI integration (OpenCode CLI) ───────────────────────────────────────────── COMMIT_PROMPT = """\ You are an expert conventional commit message writer. @@ -513,9 +554,9 @@ def generate_commit_message(diff: str, repo_path: Path, scope: Optional[str] = N scope_hint = f"Use this scope: {scope}\n" if scope else "" prompt = COMMIT_PROMPT.format(scope_hint=scope_hint, diff=filter_diff(diff)) try: - message = crush_prompt(prompt, repo_path) - except CrushError as e: - console.print(f"[red]Crush failed to generate a commit message: {e}[/]") + message = opencode_prompt(prompt, repo_path) + except OpencodeError as e: + console.print(f"[red]OpenCode failed to generate a commit message: {e}[/]") return None # Strip stray code fences if the model added them anyway. message = re.sub(r"^```[a-zA-Z]*\n|\n```$", "", message.strip()).strip() @@ -532,14 +573,14 @@ def generate_scopes(repo_path: Path, existing: Optional[dict[str, list[str]]] = else: prompt = SCOPE_PROMPT_NEW.format(filetree=filetree) try: - with console.status("[magenta]Analyzing repository with Crush...[/]"): - output = crush_prompt(prompt, repo_path) - except CrushError as e: - console.print(f"[red]Crush failed: {e}[/]") + with console.status("[magenta]Analyzing repository with OpenCode...[/]"): + output = opencode_prompt(prompt, repo_path) + except OpencodeError as e: + console.print(f"[red]OpenCode failed: {e}[/]") return None scopes = parse_scopes_response(output) if not scopes: - console.print("[red]Could not parse scopes from Crush's response[/]") + console.print("[red]Could not parse scopes from OpenCode's response[/]") if DEBUG: console.print(f"[dim]{output}[/]") return scopes @@ -637,7 +678,7 @@ def maybe_auto_refresh_scopes(repo_path: Path, repo_name: str): if current == stored: return - console.print("[yellow]↻ .gitignore changed — regenerating scopes with Crush...[/]") + console.print("[yellow]↻ .gitignore changed — regenerating scopes with OpenCode...[/]") existing = get_repo_scopes(str(repo_path)) scopes = generate_scopes(repo_path, existing) if not scopes: @@ -656,7 +697,7 @@ def cmd_version(): def cmd_help(): - console.print(f"[magenta bold]gh commit[/] [dim]v{VERSION}[/] — AI-powered scoped git commits (Crush)\n") + console.print(f"[magenta bold]gh commit[/] [dim]v{VERSION}[/] — AI-powered scoped git commits (OpenCode)\n") console.print("[cyan]Usage:[/]") console.print(" gh commit Commit changes grouped by scope") console.print(" gh commit --auto Auto-confirm all prompts") @@ -676,9 +717,9 @@ def cmd_help(): console.print(" GH_COMMIT_AUTO=1 Skip all confirmation prompts") console.print(" GH_COMMIT_PUSH=1 Auto-push after commits") console.print(" GH_COMMIT_NO_AUTO_REFRESH=1 Don't auto-regenerate scopes on .gitignore change") - console.print(" GH_COMMIT_CRUSH_CMD=... Override the Crush command") - console.print(f" GH_COMMIT_CRUSH_MODEL=... Override the Crush model (default: {DEFAULT_CRUSH_MODEL})") - console.print(" GH_COMMIT_CRUSH_TIMEOUT=... Per-prompt timeout in seconds (default: 120)") + console.print(" GH_COMMIT_OPENCODE_CMD=... Override the OpenCode command") + console.print(f" GH_COMMIT_OPENCODE_MODEL=... Override the model (default: {DEFAULT_OPENCODE_MODEL})") + console.print(" GH_COMMIT_OPENCODE_TIMEOUT=... Per-prompt timeout in seconds (default: 120)") console.print(" GH_COMMIT_DEBUG=1 Show parse diagnostics\n") console.print("[cyan]Scopes auto-refresh whenever .gitignore changes.[/]") console.print("[cyan]Legacy .github/Repo.toml or scopes.json are auto-migrated on first run.[/]") @@ -839,7 +880,7 @@ def cmd_commit(): if not repo_has_scopes(str(repo_path)): console.print("\n[yellow bold]⚠ No scopes configured for this repository[/]\n") console.print("[cyan]gh-commit organizes commits by project areas (scopes).[/]\n") - if confirm("Generate scopes now using Crush?"): + if confirm("Generate scopes now using OpenCode?"): if cmd_init() != 0: return 1 else: