From 68dc4b84a4b66b29dc5c65e0a85dea66e55cc06e Mon Sep 17 00:00:00 2001 From: Vishwa Jatania Date: Wed, 12 Aug 2026 12:09:47 -0400 Subject: [PATCH] feat: add opencode plugin support Adds a native opencode plugin that routes Snowflake prompts to Cortex Code CLI, bringing opencode to parity with the existing Claude Code and Codex integrations. New files: - plugins/cortex-code/.opencode/plugins/snowflake-cortex.ts Custom tool (cortex_run) using @opencode-ai/plugin's tool() helper. Calls execute_cortex.py via Bun shell in --codex mode. Also exports a tool.execute.before hook that intercepts direct snow sql / snowsql bash calls and redirects them through cortex. Script dir resolved via multi-location fallback (repo dev vs installed). - plugins/cortex-code/.opencode/rules/snowflake.md LLM routing rules that replace the UserPromptSubmit hook (opencode has no pre-LLM hook equivalent). Mirrors prompt_filter.py keyword logic. Instructs the LLM to call cortex_run for Snowflake work. Modified files: - execute_cortex.py: detect OPENCODE env var, set CORTEX_CODE_ENTRYPOINT="OpenCode Plugin" for analytics/tracking. - install.sh: --with-opencode flag; copies plugin TS + router scripts to ~/.config/opencode/plugins/ and rules to ~/.config/opencode/rules/. - tests/run-tests.sh: new "OpenCode plugin" section with 8 checks. - README.md + plugins/cortex-code/README.md: opencode quick-start and install instructions. .... Generated with [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code) Co-Authored-By: Cortex Code --- README.md | 13 +- install.sh | 71 +++++++++-- .../.opencode/plugins/snowflake-cortex.ts | 111 ++++++++++++++++++ .../cortex-code/.opencode/rules/snowflake.md | 45 +++++++ plugins/cortex-code/README.md | 16 ++- .../scripts/router/execute_cortex.py | 7 +- tests/run-tests.sh | 59 +++++++++- 7 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 plugins/cortex-code/.opencode/plugins/snowflake-cortex.ts create mode 100644 plugins/cortex-code/.opencode/rules/snowflake.md diff --git a/README.md b/README.md index 22c8fc9..f207b03 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Snowflake AI Kit -Connect your AI coding agent to Snowflake. Plugins for **Claude Code** and **OpenAI Codex** that automatically detect Snowflake prompts and route them to [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code-cli) — where 55+ built-in skills handle SQL, data governance, dynamic tables, ML, and more. +Connect your AI coding agent to Snowflake. Plugins for **Claude Code**, **OpenAI Codex**, and **opencode** that automatically detect Snowflake prompts and route them to [Cortex Code](https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code-cli) — where 55+ built-in skills handle SQL, data governance, dynamic tables, ML, and more. [![Claude Code](https://img.shields.io/badge/Claude%20Code-Marketplace-8A2BE2)](https://claude.com/plugins/snowflake-cortex-code) [![OpenAI Codex](https://img.shields.io/badge/OpenAI%20Codex-Marketplace-orange)](https://github.com/Snowflake-Labs/snowflake-ai-kit#openai-codex) @@ -27,6 +27,14 @@ codex plugin add snowflake-cortex-code@snowflake-ai-kit Or inside Codex, open `/plugins` and install "Snowflake Cortex Code" from the Snowflake AI Kit marketplace. +### opencode + +```bash +bash install.sh --with-opencode +``` + +Installs the `cortex_run` tool globally to `~/.config/opencode/plugins/` plus routing rules to `~/.config/opencode/rules/`. + ### That's it Ask naturally — the plugin handles routing: @@ -41,7 +49,7 @@ Non-Snowflake prompts ("fix the bug in auth.py", "write a unit test") stay in yo ## How It Works ``` -You → Claude Code / Codex → [Plugin detects Snowflake intent] → Cortex Code CLI → Snowflake +You → Claude Code / Codex / opencode → [Plugin detects Snowflake intent] → Cortex Code CLI → Snowflake ``` 1. A lightweight keyword filter runs on every prompt (~50ms, no network) @@ -88,6 +96,7 @@ The bundled installer sets up both Snowflake CLI (`snow`) and Cortex Code CLI (` | `--check` / `-Check` | Check installation status without installing | | `--with-claude` / `-WithClaude` | Also install Claude Code CLI + plugin | | `--with-codex` / `-WithCodex` | Also install OpenAI Codex CLI + plugin | +| `--with-opencode` / `-WithOpencode` | Also install opencode CLI + plugin | | `--help` / `-Help` | Show help | ## Skills diff --git a/install.sh b/install.sh index 743f493..8ac14fb 100755 --- a/install.sh +++ b/install.sh @@ -9,7 +9,7 @@ # Usage: # bash install.sh # bash install.sh --check -# bash install.sh --with-claude --with-codex +# bash install.sh --with-claude --with-codex --with-opencode # set -e @@ -115,11 +115,55 @@ install_codex_cli() { return 1 } +install_opencode_cli() { + if check_cmd opencode; then + ok "opencode CLI already installed" + return 0 + fi + + msg "Installing opencode CLI..." + if check_cmd npm; then + npm install -g opencode-ai 2>/dev/null && ok "opencode CLI installed via npm" && return 0 + fi + warn "Could not install opencode CLI (requires Node.js + npm)." + msg " Install manually: npm install -g opencode-ai" + return 1 +} + +# Copy the Snowflake Cortex Code plugin to the global opencode config dir +install_opencode_plugin() { + local SCRIPT_DIR + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + local PLUGIN_SRC="$SCRIPT_DIR/plugins/cortex-code" + + # Resolve opencode global config directory (respects XDG_CONFIG_HOME) + local OPENCODE_CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}/opencode" + + # --- plugin TS file --- + mkdir -p "$OPENCODE_CONFIG/plugins" + cp "$PLUGIN_SRC/.opencode/plugins/snowflake-cortex.ts" \ + "$OPENCODE_CONFIG/plugins/snowflake-cortex.ts" + ok "opencode plugin installed ($OPENCODE_CONFIG/plugins/snowflake-cortex.ts)" + + # --- router scripts (needed by the TS plugin via import.meta.dir) --- + local ROUTER_DEST="$OPENCODE_CONFIG/plugins/cortex-code-router/scripts/router" + mkdir -p "$ROUTER_DEST" + cp -r "$PLUGIN_SRC/scripts/router/." "$ROUTER_DEST/" + ok "opencode router scripts installed ($OPENCODE_CONFIG/plugins/cortex-code-router/scripts/router/)" + + # --- rules file (global, applies to all opencode sessions) --- + mkdir -p "$OPENCODE_CONFIG/rules" + cp "$PLUGIN_SRC/.opencode/rules/snowflake.md" \ + "$OPENCODE_CONFIG/rules/snowflake.md" + ok "opencode routing rules installed ($OPENCODE_CONFIG/rules/snowflake.md)" +} + # ─── Parse arguments ──────────────────────────────────────── CHECK_ONLY=false WITH_CLAUDE=false WITH_CODEX=false +WITH_OPENCODE=false while [ $# -gt 0 ]; do case $1 in @@ -135,6 +179,10 @@ while [ $# -gt 0 ]; do WITH_CODEX=true shift ;; + --with-opencode) + WITH_OPENCODE=true + shift + ;; --help|-h) echo "Snowflake AI Kit — Installer" echo "" @@ -147,6 +195,7 @@ while [ $# -gt 0 ]; do echo " --check, -c Check installation status without installing" echo " --with-claude Also install Claude Code CLI" echo " --with-codex Also install OpenAI Codex CLI" + echo " --with-opencode Also install opencode CLI and Snowflake plugin" echo " --help, -h Show this help" exit 0 ;; @@ -165,10 +214,11 @@ echo "" if $CHECK_ONLY; then step "Checking installation status..." - check_cmd snow && ok "Snowflake CLI (snow) installed" || warn "Snowflake CLI (snow) not found" - check_cmd cortex && ok "Cortex Code CLI (cortex) installed" || warn "Cortex Code CLI (cortex) not found" - check_cmd claude && ok "Claude Code CLI (claude) installed" || warn "Claude Code CLI (claude) not found" - check_cmd codex && ok "OpenAI Codex CLI (codex) installed" || warn "OpenAI Codex CLI (codex) not found" + check_cmd snow && ok "Snowflake CLI (snow) installed" || warn "Snowflake CLI (snow) not found" + check_cmd cortex && ok "Cortex Code CLI (cortex) installed" || warn "Cortex Code CLI (cortex) not found" + check_cmd claude && ok "Claude Code CLI (claude) installed" || warn "Claude Code CLI (claude) not found" + check_cmd codex && ok "OpenAI Codex CLI (codex) installed" || warn "OpenAI Codex CLI (codex) not found" + check_cmd opencode && ok "opencode CLI installed" || warn "opencode CLI not found" check_snowflake_auth || true echo "" exit 0 @@ -190,6 +240,13 @@ if $WITH_CODEX || check_cmd codex; then install_codex_cli || true fi +# Optional: opencode CLI + plugin +if $WITH_OPENCODE || check_cmd opencode; then + step "Installing opencode CLI and Snowflake plugin..." + install_opencode_cli || true + install_opencode_plugin || warn "Could not install opencode plugin (run: bash install.sh --with-opencode from the repo root)" +fi + # Plugin setup: add marketplace sources if the agent CLIs are present step "Setting up plugins..." @@ -227,8 +284,8 @@ if check_cmd claude; then fi fi -if ! check_cmd codex && ! check_cmd claude; then - msg " No agent CLI found. Install one with --with-claude or --with-codex" +if ! check_cmd codex && ! check_cmd claude && ! check_cmd opencode; then + msg " No agent CLI found. Install one with --with-claude, --with-codex, or --with-opencode" fi step "Checking Snowflake connection..." diff --git a/plugins/cortex-code/.opencode/plugins/snowflake-cortex.ts b/plugins/cortex-code/.opencode/plugins/snowflake-cortex.ts new file mode 100644 index 0000000..d8de0d0 --- /dev/null +++ b/plugins/cortex-code/.opencode/plugins/snowflake-cortex.ts @@ -0,0 +1,111 @@ +/** + * Snowflake Cortex Code plugin for opencode. + * + * Exposes a `cortex_run` custom tool that routes Snowflake-related prompts to + * Cortex Code CLI, plus a `tool.execute.before` hook that intercepts direct + * Snowflake CLI (snow/snowsql) bash commands and redirects them through cortex. + * + * Install via: bash install.sh --with-opencode + * Docs: https://docs.snowflake.com/en/user-guide/cortex-code/cortex-code-cli + */ + +import { type Plugin, tool } from "@opencode-ai/plugin" +import { existsSync } from "fs" +import path from "path" + +// --------------------------------------------------------------------------- +// Script directory resolution — works in repo dev AND after install.sh +// --------------------------------------------------------------------------- + +function findScriptDir(): string { + const candidates = [ + // Repo dev: plugin lives at plugins/cortex-code/.opencode/plugins/ + path.join(import.meta.dir, "../../scripts/router"), + // After install.sh --with-opencode (global): + // ~/.config/opencode/plugins/cortex-code-router/scripts/router/ + path.join(import.meta.dir, "cortex-code-router/scripts/router"), + // Absolute fallback + path.join(process.env.HOME ?? "~", ".config", "opencode", "plugins", "cortex-code-router", "scripts", "router"), + ] + return candidates.find(existsSync) ?? candidates[0] +} + +const SCRIPT_DIR = findScriptDir() + +// --------------------------------------------------------------------------- +// Snowflake CLI pattern — used by the tool.execute.before hook +// --------------------------------------------------------------------------- + +// Matches direct Snowflake CLI invocations that should go through cortex instead +const DIRECT_SNOW_CLI = /\b(snow\s+(sql|object|warehouse|database|schema|table|stage|task|streamlit|connection\s+test)|snowsql)\b/i + +// --------------------------------------------------------------------------- +// Plugin export +// --------------------------------------------------------------------------- + +export const SnowflakeCortexCode: Plugin = async () => ({ + // ------------------------------------------------------------------------- + // Custom tool: cortex_run + // ------------------------------------------------------------------------- + tool: { + cortex_run: tool({ + description: + "Execute Snowflake work via Cortex Code CLI. " + + "Use for ANY Snowflake-related request: SQL queries, databases, warehouses, schemas, " + + "tables, data governance, dynamic tables, Cortex AI, machine learning, streaming, " + + "cost analysis, semantic views, Snowpark, native apps, Streamlit in Snowflake, " + + "iceberg tables, and more. " + + "To force routing, prefix the prompt with '$cortex-run'.", + args: { + prompt: tool.schema.string().describe("The Snowflake request to execute via Cortex Code"), + envelope: tool.schema + .enum(["RO", "RW", "RESEARCH", "DEPLOY"]) + .default("RW") + .describe( + "Security envelope: RO=read-only (SELECT/SHOW/DESCRIBE), " + + "RW=read-write (default, DDL/DML allowed), " + + "RESEARCH=read+web, DEPLOY=full access" + ), + resume_last: tool.schema + .boolean() + .default(false) + .describe("Resume the previous Cortex session for multi-turn continuation"), + connection: tool.schema + .string() + .optional() + .describe("Snowflake connection name from connections.toml (uses default if omitted)"), + }, + async execute(args) { + const scriptPath = path.join(SCRIPT_DIR, "execute_cortex.py") + const cmdArgs: string[] = [ + "--prompt", args.prompt, + "--envelope", args.envelope ?? "RW", + "--codex", + ] + if (args.resume_last) cmdArgs.push("--resume-last") + if (args.connection) cmdArgs.push("--connection", args.connection) + + const result = await Bun.$`python3 ${scriptPath} ${cmdArgs}` + .env({ ...process.env, OPENCODE: "1" }) + .text() + return result.trim() + }, + }), + }, + + // ------------------------------------------------------------------------- + // Hook: redirect direct Snowflake CLI bash calls through cortex + // ------------------------------------------------------------------------- + "tool.execute.before": async (input, output) => { + if (input.tool !== "bash") return + const command: string = (output as { args?: { command?: string } }).args?.command ?? "" + if (!DIRECT_SNOW_CLI.test(command)) return + + throw new Error( + "[Snowflake Cortex Plugin] Direct Snowflake CLI commands bypass Cortex Code skills and security envelopes. " + + "Use the cortex_run tool instead — it provides 55+ built-in Snowflake skills, " + + "envelope-based permission control, and proper session management. " + + "Example: call cortex_run with your prompt and envelope='RO' for read-only operations." + ) + }, +}) diff --git a/plugins/cortex-code/.opencode/rules/snowflake.md b/plugins/cortex-code/.opencode/rules/snowflake.md new file mode 100644 index 0000000..726cd82 --- /dev/null +++ b/plugins/cortex-code/.opencode/rules/snowflake.md @@ -0,0 +1,45 @@ +# Snowflake Routing via Cortex Code + +When a prompt involves Snowflake, call the `cortex_run` tool instead of answering +directly. Cortex Code CLI has 55+ built-in skills for SQL, data governance, dynamic +tables, ML, streaming, cost intelligence, and more. + +## Route to `cortex_run` when the prompt mentions + +- **Snowflake platform**: Snowflake, Cortex, Snowpark, Snowsight, SnowSQL, `snow` CLI +- **Objects**: warehouses, databases, schemas, tables, stages, streams, tasks (in a Snowflake context) +- **Features**: dynamic tables, iceberg tables, semantic views, native apps +- **AI/ML**: Cortex Search, Cortex Analyst, Cortex Agent, Cortex AI functions, Snowflake ML +- **Governance**: data quality, data governance, data classification, lineage, masking policies +- **Compute/cost**: credits, compute costs, cost intelligence, query performance, warehouse sizing +- **Ingestion**: Snowpipe, Snowpipe Streaming, dynamic tables, OpenFlow +- **Dev tools**: Streamlit in Snowflake, Snowflake Notebooks, Snowpark Python +- **Patterns**: "show me my warehouses/databases/schemas/tables", "list all ...", "what access do I have" + +## Do NOT route to `cortex_run` for + +- Local file edits ("fix the bug in auth.py", "read config.json") +- Git operations ("git commit", "git push", "git status") +- Writing unit tests for local code unrelated to Snowflake +- npm, pip, package management for non-Snowflake code + +## Security envelopes + +Choose based on the operation: +- `RO` — read-only (SELECT, SHOW, DESCRIBE) +- `RW` — read-write (default; DDL, DML, CREATE, ALTER, DROP) +- `RESEARCH` — read + web access, no writes +- `DEPLOY` — full access (use only when explicitly requested) + +When in doubt, use `RW`. + +## Multi-turn context + +For follow-up questions on a previous Cortex answer — "keep going", "drill in", +"also show me", "and for last quarter", "fix that" — pass `resume_last: true` so +Cortex sees the prior conversation. + +## Explicit trigger + +If the user prefixes their message with `$cortex-run`, always call `cortex_run` +regardless of content. diff --git a/plugins/cortex-code/README.md b/plugins/cortex-code/README.md index 140dc98..7b9d785 100644 --- a/plugins/cortex-code/README.md +++ b/plugins/cortex-code/README.md @@ -1,6 +1,6 @@ -# Cortex Code plugin for Claude Code and OpenAI Codex +# Cortex Code plugin for Claude Code, OpenAI Codex, and opencode -Route Snowflake work from Claude Code or OpenAI Codex to Cortex Code automatically. Ask about your data naturally — the plugin detects Snowflake intent and delegates to Cortex Code where 55+ built-in skills handle the work. Non-Snowflake prompts stay in your current agent. +Route Snowflake work from Claude Code, OpenAI Codex, or opencode to Cortex Code automatically. Ask about your data naturally — the plugin detects Snowflake intent and delegates to Cortex Code where 55+ built-in skills handle the work. Non-Snowflake prompts stay in your current agent. ## How It Works @@ -55,6 +55,18 @@ codex plugin add snowflake-cortex-code@snowflake-ai-kit Or inside Codex, open `/plugins` and install "Snowflake Cortex Code" from the Snowflake AI Kit marketplace. +### opencode + +```bash +bash install.sh --with-opencode +``` + +This copies the plugin to `~/.config/opencode/plugins/` and routing rules to `~/.config/opencode/rules/` — both are loaded globally for all opencode sessions. + +> **Note:** opencode has no pre-LLM hook equivalent to Claude Code's `UserPromptSubmit`, so routing is LLM-driven via the rules file plus a `tool.execute.before` hook that blocks direct `snow sql`/`snowsql` bash calls. In practice the LLM reliably calls `cortex_run` for Snowflake work given the rules file. + +To update, re-run `bash install.sh --with-opencode`. + ## Security Model The router wraps Cortex execution with a security layer. Three approval modes: diff --git a/plugins/cortex-code/scripts/router/execute_cortex.py b/plugins/cortex-code/scripts/router/execute_cortex.py index c1e5f72..7226f06 100755 --- a/plugins/cortex-code/scripts/router/execute_cortex.py +++ b/plugins/cortex-code/scripts/router/execute_cortex.py @@ -385,6 +385,8 @@ def execute_cortex_streaming(prompt: str, connection: Optional[str] = None, env["CORTEX_CODE_ENTRYPOINT"] = "Claude Code Plugin" elif os.environ.get("PLUGIN_ROOT") or os.environ.get("CLAUDE_PLUGIN_ROOT"): env["CORTEX_CODE_ENTRYPOINT"] = "Codex Plugin" + elif os.environ.get("OPENCODE"): + env["CORTEX_CODE_ENTRYPOINT"] = "OpenCode Plugin" else: env["CORTEX_CODE_ENTRYPOINT"] = "Unknown" @@ -592,7 +594,10 @@ def _run_codex_mode(args): }) + "\n" env = os.environ.copy() - env["CORTEX_CODE_ENTRYPOINT"] = "Codex Plugin" + if os.environ.get("OPENCODE"): + env["CORTEX_CODE_ENTRYPOINT"] = "OpenCode Plugin" + else: + env["CORTEX_CODE_ENTRYPOINT"] = "Codex Plugin" perm_mode = "--dangerously-allow-all-tool-calls --no-mcp" debug_cmd = f"cortex --output-format stream-json --input-format stream-json {perm_mode} (envelope={args.envelope})" diff --git a/tests/run-tests.sh b/tests/run-tests.sh index ef751f4..848ac8b 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -253,7 +253,60 @@ else fail "config.yaml.example is non-empty" fi -# === 5. Unit tests ================================================= +# === 5. OpenCode plugin ============================================ + +section "OpenCode plugin" + +OPENCODE_PLUGIN_DIR="$PLUGIN_DIR/.opencode" +OPENCODE_TS="$OPENCODE_PLUGIN_DIR/plugins/snowflake-cortex.ts" +OPENCODE_RULES="$OPENCODE_PLUGIN_DIR/rules/snowflake.md" + +check ".opencode/plugins/snowflake-cortex.ts exists" test -f "$OPENCODE_TS" +check ".opencode/rules/snowflake.md exists" test -f "$OPENCODE_RULES" + +# TS file must export a Plugin and define cortex_run +if grep -q "cortex_run" "$OPENCODE_TS" 2>/dev/null; then + pass "snowflake-cortex.ts exports cortex_run tool" +else + fail "snowflake-cortex.ts exports cortex_run tool" +fi + +# TS file must set OPENCODE env var for entrypoint tracking +if grep -q "OPENCODE" "$OPENCODE_TS" 2>/dev/null; then + pass "snowflake-cortex.ts sets OPENCODE env var" +else + fail "snowflake-cortex.ts sets OPENCODE env var" +fi + +# TS file must include the tool.execute.before hook +if grep -q "tool.execute.before" "$OPENCODE_TS" 2>/dev/null; then + pass "snowflake-cortex.ts defines tool.execute.before hook" +else + fail "snowflake-cortex.ts defines tool.execute.before hook" +fi + +# Rules file must reference cortex_run +if grep -q "cortex_run" "$OPENCODE_RULES" 2>/dev/null; then + pass "snowflake.md references cortex_run" +else + fail "snowflake.md references cortex_run" +fi + +# execute_cortex.py must handle OPENCODE entrypoint +if grep -q "OPENCODE" "$ROUTER_DIR/execute_cortex.py" 2>/dev/null; then + pass "execute_cortex.py handles OPENCODE entrypoint" +else + fail "execute_cortex.py handles OPENCODE entrypoint" +fi + +# install.sh must include --with-opencode flag +if grep -q "with-opencode" "$REPO_ROOT/install.sh" 2>/dev/null; then + pass "install.sh includes --with-opencode flag" +else + fail "install.sh includes --with-opencode flag" +fi + +# === 6. Unit tests ================================================= section "Unit tests" @@ -295,7 +348,7 @@ else fi fi -# === 6. Integration tests (optional, requires cortex CLI + Snowflake connection) === +# === 7. Integration tests (optional, requires cortex CLI + Snowflake connection) === section "Integration tests" @@ -326,7 +379,7 @@ else fi fi -# === 7. Snowflake connection ======================================= +# === 8. Snowflake connection ======================================= section "Snowflake connection"