diff --git a/.grok-plugin/marketplace.json b/.grok-plugin/marketplace.json index e8340b7..481e75b 100644 --- a/.grok-plugin/marketplace.json +++ b/.grok-plugin/marketplace.json @@ -463,8 +463,37 @@ "path": "./plugins/grokhunter-termux-connect" }, "homepage": "https://finecomputer14451.github.io/GrokHunter/", - "keywords": ["grokhunter termux connect", "grokhunter termux mcp", "termux connect grokhunter"], - "domains": ["finecomputer14451.github.io"] + "keywords": [ + "grokhunter termux connect", + "grokhunter termux mcp", + "termux connect grokhunter" + ], + "domains": [ + "finecomputer14451.github.io" + ] + }, + { + "name": "grokhunter-grok-build-connect", + "description": "Bridge Grok Bot to local Grok Build (grok CLI) via HTTP MCP + tunnel. Termux and desktop.", + "category": "development", + "version": "1.0.0", + "author": { + "name": "FineComputer14451", + "url": "https://github.com/FineComputer14451/GrokHunter" + }, + "source": { + "type": "local", + "path": "./plugins/grokhunter-grok-build-connect" + }, + "homepage": "https://finecomputer14451.github.io/GrokHunter/", + "keywords": [ + "grokhunter grok build connect", + "grok build mcp", + "grok connect" + ], + "domains": [ + "finecomputer14451.github.io" + ] } ] } diff --git a/.grok-plugin/plugin-index.json b/.grok-plugin/plugin-index.json index 76f1e64..f7adad5 100644 --- a/.grok-plugin/plugin-index.json +++ b/.grok-plugin/plugin-index.json @@ -748,6 +748,30 @@ } ], "version": "1.0.0" + }, + { + "agents": [], + "commands": [ + { + "description": "Install Grok Build Connect MCP on Termux or desktop", + "name": "grok-build-connect-install" + }, + { + "description": "Tunnel Grok Build Connect MCP with Cloudflare", + "name": "grok-build-connect-tunnel" + } + ], + "hooks": [], + "lspServers": [], + "mcpServers": [], + "name": "grokhunter-grok-build-connect", + "skills": [ + { + "description": "Bridge Grok Bot to the local Grok Build (grok) CLI via authenticated HTTP MCP + Cloudflare/ngrok tunnel on Termux or desktop.", + "name": "grok-build-connect" + } + ], + "version": "1.0.0" } ] } diff --git a/docs/MARKETPLACE.md b/docs/MARKETPLACE.md index 9d12a42..55dfef2 100644 --- a/docs/MARKETPLACE.md +++ b/docs/MARKETPLACE.md @@ -56,6 +56,7 @@ grok plugin install grokhunter-ci --trust | `grokhunter-desktop` | GrokHunter X11 desktop — grokhunter binds, nh-x11, bwrap black-screen triage (rootless). | | `grokhunter-overlay` | GrokHunter overlay cache — install.sh --overlay-only, MODULES_VERSION, PATH wrappers. | | `grokhunter-tookie` | Authorized public username OSINT with Tookie-OSINT (Sherlock-class). Scoped GrokHunter mod… | +| `grokhunter-grok-build-connect` | Bridge Grok Bot ↔ local `grok` CLI via HTTP MCP + tunnel (Termux + desktop). | | `grokhunter-termux-connect` | Bridge Grok Bot ↔ Termux via HTTP MCP + tunnel (not native Local Computer). | | `grokhunter-full` | Complete GrokHunter Grok Build pack — core, Coding Team, all lab specialists, desktop, ove… | @@ -88,6 +89,7 @@ Native Grok Bot Local Computer is Mac/Windows only. To drive GrokHunter on Termu ```bash grok plugin install grokhunter-termux-connect --trust +grok plugin install grokhunter-grok-build-connect --trust bash ~/.… # see plugin README / skill termux-connect ``` diff --git a/plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json b/plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json new file mode 100644 index 0000000..0e35266 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/.cursor-plugin/plugin.json @@ -0,0 +1,22 @@ +{ + "name": "grokhunter-grok-build-connect", + "version": "1.0.0", + "description": "Bridge Grok Bot to local Grok Build (grok CLI) via authenticated HTTP MCP + tunnel. Termux and desktop. Not affiliated with xAI / OffSec / Termux / jorexdeveloper.", + "author": { + "name": "FineComputer14451", + "url": "https://github.com/FineComputer14451/GrokHunter" + }, + "homepage": "https://finecomputer14451.github.io/GrokHunter/", + "repository": "https://github.com/FineComputer14451/GrokHunter", + "license": "MIT", + "keywords": [ + "grokhunter grok build connect", + "grok build mcp", + "grok connect" + ], + "logo": "assets/logo.png", + "displayName": "GrokHunter Grok Build Connect", + "category": "developer-tools", + "skills": "./skills/", + "commands": "./commands/" +} diff --git a/plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json b/plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json new file mode 100644 index 0000000..d2487ed --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/.grok-plugin/plugin.json @@ -0,0 +1,18 @@ +{ + "name": "grokhunter-grok-build-connect", + "version": "1.0.0", + "description": "Bridge Grok Bot to local Grok Build (grok CLI) via authenticated HTTP MCP + tunnel. Termux and desktop. Not affiliated with xAI / OffSec / Termux / jorexdeveloper.", + "author": { + "name": "FineComputer14451", + "url": "https://github.com/FineComputer14451/GrokHunter" + }, + "homepage": "https://finecomputer14451.github.io/GrokHunter/", + "repository": "https://github.com/FineComputer14451/GrokHunter", + "license": "MIT", + "keywords": [ + "grokhunter grok build connect", + "grok build mcp", + "grok connect" + ], + "logo": "assets/logo.png" +} diff --git a/plugins/grokhunter-grok-build-connect/LICENSE b/plugins/grokhunter-grok-build-connect/LICENSE new file mode 100644 index 0000000..3a6be55 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 GrokHunter contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/grokhunter-grok-build-connect/README.md b/plugins/grokhunter-grok-build-connect/README.md new file mode 100644 index 0000000..4626999 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/README.md @@ -0,0 +1,28 @@ +# grokhunter-grok-build-connect + +Bridge **Grok Bot → local Grok Build (`grok` CLI)** via authenticated HTTP MCP + tunnel. +Works on **Termux and desktop**. Not affiliated with xAI / OffSec / Termux / jorexdeveloper. + +## Why + +Grok Bot’s native Local Computer is Mac/Windows. This plugin exposes your installed +`grok` CLI (phone or desktop) over HTTPS MCP so the Bot can call version/plugins/ask tools. + +## Quick start + +```bash +curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +``` + +Default port **8766** (keeps Termux Connect on 8765 free). + +## Security + +- Listens on `127.0.0.1` only +- Bearer token required (min 24 chars) +- Path jail: `$HOME` (+ `$PREFIX` on Termux) +- Never commit `~/.grokhunter-grok-build-connect/token` + +## Credits + +See [CREDITS.md](https://github.com/FineComputer14451/GrokHunter/blob/main/CREDITS.md). diff --git a/plugins/grokhunter-grok-build-connect/assets/logo.png b/plugins/grokhunter-grok-build-connect/assets/logo.png new file mode 100644 index 0000000..da352aa Binary files /dev/null and b/plugins/grokhunter-grok-build-connect/assets/logo.png differ diff --git a/plugins/grokhunter-grok-build-connect/commands/grok-build-connect-install.md b/plugins/grokhunter-grok-build-connect/commands/grok-build-connect-install.md new file mode 100644 index 0000000..dc0186f --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/commands/grok-build-connect-install.md @@ -0,0 +1,11 @@ +--- +description: Install Grok Build Connect MCP on Termux or desktop +--- +Run: + +```bash +bash skills/grok-build-connect/scripts/install.sh +~/.grokhunter-grok-build-connect/start.sh +``` + +Do **not** print `~/.grokhunter-grok-build-connect/token`. Next: `/grok-build-connect-tunnel`. diff --git a/plugins/grokhunter-grok-build-connect/commands/grok-build-connect-tunnel.md b/plugins/grokhunter-grok-build-connect/commands/grok-build-connect-tunnel.md new file mode 100644 index 0000000..134f8da --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/commands/grok-build-connect-tunnel.md @@ -0,0 +1,8 @@ +--- +description: Tunnel Grok Build Connect MCP with Cloudflare +--- +```bash +bash skills/grok-build-connect/scripts/tunnel-cloudflare.sh +``` + +Copy `https://….trycloudflare.com`, append `/mcp`. Bearer from `~/.grokhunter-grok-build-connect/token`. diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/SKILL.md b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/SKILL.md new file mode 100644 index 0000000..0fb9572 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/SKILL.md @@ -0,0 +1,41 @@ +--- +name: grok-build-connect +description: >- + Use when bridging Grok Bot to the local Grok Build (`grok`) CLI via HTTP MCP + + Cloudflare/ngrok tunnel on Termux or desktop. +--- +# Grok Build Connect (MVP) + +Expose the **local `grok` CLI** to Grok Bot through an authenticated **HTTP MCP** +(and a tunnel). Works on **Termux and desktop**. Port default **8766** (Termux +Connect uses 8765). + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +``` + +Or from a checkout: + +```bash +bash skills/grok-build-connect/scripts/install.sh +~/.grokhunter-grok-build-connect/start.sh +bash skills/grok-build-connect/scripts/tunnel-cloudflare.sh +``` + +## Grok Bot connector + +1. Add custom connector (chat / desktop / grok.com/connectors) +2. URL: `https:///mcp` +3. Auth: Bearer from `~/.grokhunter-grok-build-connect/token` — never paste in chat + +## Tools + +`grok_version`, `grok_plugin_list`, `grok_mcp_list`, `grok_mcp_doctor`, `grok_ask`, `list_dir`, `read_file` + +## Hard rules + +- Never print the bearer token or secrets.env +- Not Magisk/root; not native Android Local Computer +- Coding lab only diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh new file mode 100755 index 0000000..0b1d319 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/bootstrap.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Grok Build Connect — one-shot bootstrap (Termux or desktop). +# curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +set -euo pipefail + +REPO_URL="${GROKHUNTER_REPO_URL:-https://github.com/FineComputer14451/GrokHunter.git}" +BRANCH="${GROKHUNTER_BRANCH:-main}" +TARGET_DIR="${GROKHUNTER_DIR:-$HOME/GrokHunter}" +START_MCP=1 +START_TUNNEL=0 +START_FOREGROUND=0 +REGISTER_MARKETPLACE=1 +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +usage() { + cat <<'U' +GrokHunter Grok Build Connect bootstrap (Termux + desktop) + +Options: + --dir PATH Clone/update location (default: ~/GrokHunter) + --tunnel Also start Cloudflare quick tunnel (tmux session: gbc-tun) + --no-start Do not auto-start MCP in tmux + --start Start MCP in the foreground (blocks; skips tmux) + --no-marketplace Skip grok plugin marketplace add + -h, --help Show help + +Env: + GROKHUNTER_DIR, GROKHUNTER_REPO_URL, GROKHUNTER_BRANCH, GROK_BUILD_CONNECT_PORT +U +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) TARGET_DIR="$2"; shift 2 ;; + --tunnel) START_TUNNEL=1; shift ;; + --no-start) START_MCP=0; shift ;; + --start) START_FOREGROUND=1; START_MCP=0; shift ;; + --no-marketplace) REGISTER_MARKETPLACE=0; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;; + esac +done + +need() { + command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; } +} +need git +need python3 +need bash + +SCRIPTS="" +resolve_scripts() { + if [[ -f "$TARGET_DIR/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/skills/grok-build-connect/scripts" + elif [[ -f "$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts" + else + return 1 + fi +} + +echo "==> Ensuring GrokHunter at $TARGET_DIR ($BRANCH)" +if [[ -d "$TARGET_DIR/.git" ]]; then + git -C "$TARGET_DIR" fetch --depth 1 origin "$BRANCH" + git -C "$TARGET_DIR" checkout -B "$BRANCH" "origin/$BRANCH" 2>/dev/null \ + || { git -C "$TARGET_DIR" checkout "$BRANCH" 2>/dev/null || true; git -C "$TARGET_DIR" pull --ff-only origin "$BRANCH" || true; } +else + mkdir -p "$(dirname "$TARGET_DIR")" + if [[ -e "$TARGET_DIR" ]] && [[ ! -d "$TARGET_DIR/.git" ]]; then + echo "Path exists but is not a git repo: $TARGET_DIR" >&2 + exit 1 + fi + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$TARGET_DIR" +fi + +if ! resolve_scripts; then + echo "grok-build-connect scripts not found in $TARGET_DIR" >&2 + exit 1 +fi + +echo "==> Installing Grok Build Connect MCP" +bash "$SCRIPTS/install.sh" + +echo "==> Ensuring cloudflared" +bash "$SCRIPTS/ensure-cloudflared.sh" || echo "WARN: cloudflared ensure failed" >&2 + +if [[ "$REGISTER_MARKETPLACE" -eq 1 ]] && command -v grok >/dev/null 2>&1; then + grok plugin marketplace add FineComputer14451/GrokHunter 2>/dev/null || true + grok plugin install grokhunter-grok-build-connect --trust 2>/dev/null \ + || grok plugin install "$TARGET_DIR/plugins/grokhunter-grok-build-connect" --trust 2>/dev/null \ + || echo "WARN: plugin install skipped" >&2 +fi + +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" + +if [[ "$START_FOREGROUND" -eq 1 ]]; then + exec "$PREFIX_DIR/start.sh" +fi + +if [[ "$START_MCP" -eq 1 ]]; then + if ! command -v tmux >/dev/null 2>&1; then + echo "tmux missing — start manually: $PREFIX_DIR/start.sh" >&2 + else + tmux has-session -t gbc 2>/dev/null && tmux kill-session -t gbc + echo "==> Starting MCP in tmux session 'gbc'" + tmux new-session -d -s gbc "$PREFIX_DIR/start.sh" + for i in $(seq 1 20); do + curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1 && break + sleep 0.5 + done + if curl -fsS "http://127.0.0.1:${PORT}/healthz"; then + echo + echo "MCP healthy on :$PORT" + else + echo "WARN: MCP healthz not ready — tmux attach -t gbc" >&2 + fi + fi +fi + +if [[ "$START_TUNNEL" -eq 1 ]]; then + if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "Cannot start tunnel — MCP unhealthy" >&2 + exit 1 + fi + if command -v tmux >/dev/null 2>&1; then + tmux has-session -t gbc-tun 2>/dev/null && tmux kill-session -t gbc-tun + echo "==> Starting Cloudflare tunnel in tmux session 'gbc-tun'" + tmux new-session -d -s gbc-tun "bash '$SCRIPTS/tunnel-cloudflare.sh'" + echo "Attach: tmux attach -t gbc-tun" + else + exec bash "$SCRIPTS/tunnel-cloudflare.sh" + fi +fi + +cat </mcp + Token: $PREFIX_DIR/token (do not paste into chat) +EOF diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh new file mode 100755 index 0000000..30fda9a --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/ensure-cloudflared.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Ensure cloudflared is on PATH (Termux pkg or GitHub release binary). +set -euo pipefail + +if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 +fi + +BIN_DIR="${TERMUX_CONNECT_BIN:-$HOME/.grokhunter-termux-connect/bin}" +mkdir -p "$BIN_DIR" +TARGET="$BIN_DIR/cloudflared" + +if command -v pkg >/dev/null 2>&1; then + echo "==> Trying: pkg install cloudflared" + if pkg install -y cloudflared 2>/dev/null; then + if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 + fi + fi +fi + +arch="$(uname -m)" +case "$arch" in + aarch64|arm64) asset="cloudflared-linux-arm64" ;; + armv7l|armv8l|arm) asset="cloudflared-linux-arm" ;; + x86_64|amd64) asset="cloudflared-linux-amd64" ;; + i686|i386) asset="cloudflared-linux-386" ;; + *) + echo "Unsupported arch for cloudflared binary: $arch" >&2 + echo "Install manually: https://github.com/cloudflare/cloudflared/releases" >&2 + exit 1 + ;; +esac + +url="https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}" +echo "==> Downloading $url" +if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$TARGET" "$url" +elif command -v wget >/dev/null 2>&1; then + wget -q -O "$TARGET" "$url" +else + echo "Need curl or wget to fetch cloudflared" >&2 + exit 1 +fi +chmod +x "$TARGET" + +# prepend bin dir for this shell / recommend PATH +case ":$PATH:" in + *":$BIN_DIR:"*) ;; + *) export PATH="$BIN_DIR:$PATH" ;; +esac + +# persist hint +profile_snip="$HOME/.grokhunter-termux-connect/path.sh" +cat >"$profile_snip" <

/dev/null; then + echo "[ -f \"$profile_snip\" ] && . \"$profile_snip\"" >>"$HOME/.bashrc" +fi + +if ! command -v cloudflared >/dev/null 2>&1; then + echo "Installed to $TARGET — add to PATH: export PATH=\"$BIN_DIR:\$PATH\"" >&2 + # still usable via absolute path; create shim if ~/bin exists +fi +echo "cloudflared: $TARGET" +echo "CF_BIN=$TARGET" diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh new file mode 100755 index 0000000..ca14406 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Install Grok Build Connect MCP (Termux or desktop). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" +VENV="$PREFIX_DIR/venv" +TOKEN_FILE="$PREFIX_DIR/token" +ENV_FILE="$PREFIX_DIR/env" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +mkdir -p "$PREFIX_DIR" +python3 -m venv "$VENV" +# shellcheck disable=SC1091 +source "$VENV/bin/activate" +pip install -U pip +pip install -r "$ROOT/requirements.txt" +cp -f "$ROOT/server.py" "$PREFIX_DIR/server.py" +chmod +x "$PREFIX_DIR/server.py" + +if [[ ! -f "$TOKEN_FILE" ]] || [[ "$(wc -c <"$TOKEN_FILE" | tr -d ' ')" -lt 24 ]]; then + python3 -c 'import secrets; print(secrets.token_urlsafe(32))' >"$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" +fi +TOKEN="$(tr -d '[:space:]' <"$TOKEN_FILE")" + +cat >"$ENV_FILE" <"$PREFIX_DIR/start.sh" <<'EOS' +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/venv/bin/activate" +# shellcheck disable=SC1091 +source "$DIR/env" +if ! command -v grok >/dev/null 2>&1; then + echo "WARN: grok not on PATH — install Grok Build before using ask/plugin tools" >&2 +fi +exec python3 "$DIR/server.py" +EOS +chmod +x "$PREFIX_DIR/start.sh" + +cat >"$PREFIX_DIR/print-connector.txt" </mcp + Auth: Bearer (from $TOKEN_FILE — never chat it) + Port: $PORT (default; Termux Connect uses 8765) + +Start: $PREFIX_DIR/start.sh +Tunnel: bash $ROOT/tunnel-cloudflare.sh +Health: curl -s http://127.0.0.1:${PORT}/healthz +EOF + +echo "Installed → $PREFIX_DIR" +echo "Token file (mode 600): $TOKEN_FILE" +echo "Start: $PREFIX_DIR/start.sh" +echo "Works on Termux and desktop. Expose with tunnel-*.sh, register HTTPS URL in Grok Bot." diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt new file mode 100644 index 0000000..75f9516 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/requirements.txt @@ -0,0 +1,2 @@ +mcp>=1.9.0,<2 +uvicorn>=0.30.0 diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py new file mode 100755 index 0000000..97233df --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/server.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""GrokHunter Grok Build Connect — HTTP MCP bridging Grok Bot to the grok CLI. + +Works on Termux and desktop (Linux/macOS). Binds 127.0.0.1 by default; +put Cloudflare/ngrok in front. Requires Authorization: Bearer . +""" +from __future__ import annotations + +import os +import re +import secrets +import shutil +import subprocess +import sys +from pathlib import Path + +HOST = os.environ.get("GROK_BUILD_CONNECT_HOST", "127.0.0.1") +PORT = int(os.environ.get("GROK_BUILD_CONNECT_PORT", "8766")) +TOKEN = os.environ.get("GROK_BUILD_CONNECT_TOKEN", "").strip() +HOME = Path(os.environ.get("HOME", str(Path.home()))).resolve() +EXTRA_ROOTS = [ + Path(p).resolve() + for p in os.environ.get("GROK_BUILD_CONNECT_ROOTS", "").split(":") + if p.strip() +] +MAX_BYTES = int(os.environ.get("GROK_BUILD_CONNECT_MAX_BYTES", str(256 * 1024))) +ASK_TIMEOUT = int(os.environ.get("GROK_BUILD_CONNECT_ASK_TIMEOUT", "180")) +ASK_MAX_CHARS = int(os.environ.get("GROK_BUILD_CONNECT_ASK_MAX_CHARS", "8000")) +CWD_DEFAULT = Path(os.environ.get("GROK_BUILD_CONNECT_CWD", str(HOME))).expanduser().resolve() + +# Optional override for grok binary +GROK_BIN = os.environ.get("GROK_BUILD_CONNECT_GROK", "").strip() + + +def _grok() -> str: + if GROK_BIN: + return GROK_BIN + found = shutil.which("grok") + if not found: + raise FileNotFoundError( + "grok CLI not found on PATH. Install Grok Build, then re-run start.sh." + ) + return found + + +def _roots() -> list[Path]: + roots = [HOME, CWD_DEFAULT, *EXTRA_ROOTS] + prefix = os.environ.get("PREFIX") + if prefix: + roots.append(Path(prefix).resolve()) + out: list[Path] = [] + seen = set() + for r in roots: + s = str(r) + if s not in seen: + seen.add(s) + out.append(r) + return out + + +def _jail(path: str) -> Path: + raw = Path(path).expanduser() + if not raw.is_absolute(): + raw = CWD_DEFAULT / raw + resolved = raw.resolve() + for root in _roots(): + try: + resolved.relative_to(root) + return resolved + except ValueError: + continue + raise PermissionError(f"path outside jail: {path}") + + +def _check_token() -> None: + if not TOKEN or len(TOKEN) < 24: + print( + "GROK_BUILD_CONNECT_TOKEN missing or too short (min 24 chars).\n" + 'Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"', + file=sys.stderr, + ) + sys.exit(2) + + +def _run(argv: list[str], timeout: int | None = None, cwd: Path | None = None) -> str: + try: + proc = subprocess.run( + argv, + capture_output=True, + text=True, + timeout=timeout or 90, + cwd=str(cwd or CWD_DEFAULT), + env={**os.environ, "TERM": os.environ.get("TERM", "xterm-256color"), "CI": "1"}, + ) + except FileNotFoundError as e: + return f"not found: {e}" + except subprocess.TimeoutExpired: + return f"timeout after {timeout or 90}s" + out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "") + raw = out.encode("utf-8", errors="replace") + if len(raw) > MAX_BYTES: + out = raw[:MAX_BYTES].decode("utf-8", errors="replace") + "\n…[truncated]" + return f"exit {proc.returncode}\n{out}".rstrip() + + +def main() -> None: + _check_token() + try: + from mcp.server.fastmcp import FastMCP + from mcp.server.transport_security import TransportSecuritySettings + from starlette.middleware.base import BaseHTTPMiddleware + from starlette.requests import Request + from starlette.responses import JSONResponse + from starlette.routing import Route + import uvicorn + except ImportError as e: + print("Missing deps. Run: pip install -r requirements.txt\n" + str(e), file=sys.stderr) + sys.exit(1) + + # Cloudflare quick tunnels send Host: *.trycloudflare.com (see termux-connect fix). + transport_security = TransportSecuritySettings(enable_dns_rebinding_protection=False) + extra_hosts = [ + h.strip() + for h in os.environ.get("GROK_BUILD_CONNECT_ALLOWED_HOSTS", "").split(",") + if h.strip() + ] + if extra_hosts: + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=[ + f"{HOST}:{PORT}", + HOST, + "localhost", + f"localhost:{PORT}", + "127.0.0.1:*", + "localhost:*", + *extra_hosts, + ], + ) + + mcp = FastMCP( + "grokhunter-grok-build-connect", + instructions=( + "Scoped tools for the local Grok Build (`grok`) CLI on Termux or desktop. " + "Prefer grok_version / grok_plugin_list / grok_ask. Paths are jailed to HOME." + ), + host=HOST, + port=PORT, + streamable_http_path="/mcp", + stateless_http=True, + transport_security=transport_security, + ) + + @mcp.tool() + def grok_version() -> str: + """Return `grok --version` and resolved binary path.""" + g = _grok() + return f"binary={g}\n" + _run([g, "--version"], timeout=30) + + @mcp.tool() + def grok_plugin_list() -> str: + """List installed Grok Build plugins (`grok plugin list`).""" + return _run([_grok(), "plugin", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_list() -> str: + """List MCP servers configured in Grok Build (`grok mcp list`).""" + return _run([_grok(), "mcp", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_doctor() -> str: + """Diagnose Grok Build MCP config (`grok mcp doctor`).""" + return _run([_grok(), "mcp", "doctor"], timeout=90) + + @mcp.tool() + def grok_ask(prompt: str, model: str = "", cwd: str = "") -> str: + """Single-turn headless `grok -p` (prints response and exits). Caps prompt length.""" + p = (prompt or "").strip() + if not p: + return "empty prompt" + if len(p) > ASK_MAX_CHARS: + return f"prompt too long (max {ASK_MAX_CHARS} chars)" + work = _jail(cwd) if cwd.strip() else CWD_DEFAULT + if not work.is_dir(): + return f"cwd not a directory: {work}" + argv = [_grok(), "-p", p, "--output-format", "text"] + if model.strip(): + argv.extend(["-m", model.strip()]) + return _run(argv, timeout=ASK_TIMEOUT, cwd=work) + + @mcp.tool() + def list_dir(path: str = ".") -> str: + """List a directory under the jail (HOME / PREFIX / configured cwd).""" + target = _jail(path) + if not target.is_dir(): + return f"not a directory: {target}" + lines = [] + for child in sorted(target.iterdir(), key=lambda p: p.name.lower())[:500]: + kind = "d" if child.is_dir() else "f" + lines.append(f"{kind}\t{child.name}") + return "\n".join(lines) or "(empty)" + + @mcp.tool() + def read_file(path: str, max_bytes: int = 65536) -> str: + """Read a text file under the jail (capped).""" + target = _jail(path) + if not target.is_file(): + return f"not a file: {target}" + n = min(max(1, max_bytes), MAX_BYTES) + data = target.read_bytes()[: n + 1] + truncated = len(data) > n + text = data[:n].decode("utf-8", errors="replace") + if truncated: + text += "\n…[truncated]" + return text + + class BearerAuth(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.url.path in {"/healthz", "/"}: + return await call_next(request) + auth = request.headers.get("authorization", "") + expected = f"Bearer {TOKEN}" + if not secrets.compare_digest(auth, expected): + return JSONResponse({"error": "unauthorized"}, status_code=401) + return await call_next(request) + + async def healthz(request: Request): + grok_ok = False + try: + _grok() + grok_ok = True + except Exception: + grok_ok = False + return JSONResponse( + { + "ok": True, + "service": "grokhunter-grok-build-connect", + "grok_on_path": grok_ok, + "port": PORT, + } + ) + + app = mcp.streamable_http_app() + app.add_middleware(BearerAuth) + app.routes.insert(0, Route("/healthz", healthz, methods=["GET"])) + + print( + f"grok-build-connect listening on http://{HOST}:{PORT}/mcp " + f"(cwd={CWD_DEFAULT}, jail={_roots()})", + flush=True, + ) + uvicorn.run(app, host=HOST, port=PORT, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh new file mode 100755 index 0000000..5c1a602 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-cloudflare.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Cloudflare quick tunnel in front of Grok Build Connect MCP. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +ensure_log="$(mktemp)" +bash "$ROOT/ensure-cloudflared.sh" | tee "$ensure_log" +CF="" +if command -v cloudflared >/dev/null 2>&1; then + CF="$(command -v cloudflared)" +elif [[ -x "$HOME/.grokhunter-grok-build-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-grok-build-connect/bin/cloudflared" +elif [[ -x "$HOME/.grokhunter-termux-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-termux-connect/bin/cloudflared" +else + CF="$(grep '^CF_BIN=' "$ensure_log" | tail -n1 | cut -d= -f2- || true)" +fi +rm -f "$ensure_log" + +if [[ -z "${CF:-}" || ! -x "$CF" ]]; then + echo "cloudflared still missing" >&2 + exit 1 +fi + +for i in $(seq 1 15); do + if curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + break + fi + sleep 1 +done +if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "MCP not healthy on :${PORT} — start it first (tmux attach -t gbc)" >&2 + exit 1 +fi + +echo "Starting quick tunnel → http://127.0.0.1:${PORT}" +echo "Copy the https://….trycloudflare.com URL, append /mcp" +echo "Bearer token file: ~/.grokhunter-grok-build-connect/token (do not paste into chat)" +exec "$CF" tunnel --url "http://127.0.0.1:${PORT}" diff --git a/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh new file mode 100755 index 0000000..9e88937 --- /dev/null +++ b/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/tunnel-ngrok.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" +if ! command -v ngrok >/dev/null 2>&1; then + echo "Install ngrok or use tunnel-cloudflare.sh" + exit 1 +fi +echo "Starting ngrok → http://127.0.0.1:${PORT}" +echo "Copy the https URL, append /mcp, Bearer from ~/.grokhunter-grok-build-connect/token" +exec ngrok http "$PORT" diff --git a/skills/grok-build-connect/SKILL.md b/skills/grok-build-connect/SKILL.md new file mode 100644 index 0000000..0fb9572 --- /dev/null +++ b/skills/grok-build-connect/SKILL.md @@ -0,0 +1,41 @@ +--- +name: grok-build-connect +description: >- + Use when bridging Grok Bot to the local Grok Build (`grok`) CLI via HTTP MCP + + Cloudflare/ngrok tunnel on Termux or desktop. +--- +# Grok Build Connect (MVP) + +Expose the **local `grok` CLI** to Grok Bot through an authenticated **HTTP MCP** +(and a tunnel). Works on **Termux and desktop**. Port default **8766** (Termux +Connect uses 8765). + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +``` + +Or from a checkout: + +```bash +bash skills/grok-build-connect/scripts/install.sh +~/.grokhunter-grok-build-connect/start.sh +bash skills/grok-build-connect/scripts/tunnel-cloudflare.sh +``` + +## Grok Bot connector + +1. Add custom connector (chat / desktop / grok.com/connectors) +2. URL: `https:///mcp` +3. Auth: Bearer from `~/.grokhunter-grok-build-connect/token` — never paste in chat + +## Tools + +`grok_version`, `grok_plugin_list`, `grok_mcp_list`, `grok_mcp_doctor`, `grok_ask`, `list_dir`, `read_file` + +## Hard rules + +- Never print the bearer token or secrets.env +- Not Magisk/root; not native Android Local Computer +- Coding lab only diff --git a/skills/grok-build-connect/scripts/bootstrap.sh b/skills/grok-build-connect/scripts/bootstrap.sh new file mode 100755 index 0000000..0b1d319 --- /dev/null +++ b/skills/grok-build-connect/scripts/bootstrap.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Grok Build Connect — one-shot bootstrap (Termux or desktop). +# curl -fsSL https://raw.githubusercontent.com/FineComputer14451/GrokHunter/main/skills/grok-build-connect/scripts/bootstrap.sh | bash -s -- --tunnel +set -euo pipefail + +REPO_URL="${GROKHUNTER_REPO_URL:-https://github.com/FineComputer14451/GrokHunter.git}" +BRANCH="${GROKHUNTER_BRANCH:-main}" +TARGET_DIR="${GROKHUNTER_DIR:-$HOME/GrokHunter}" +START_MCP=1 +START_TUNNEL=0 +START_FOREGROUND=0 +REGISTER_MARKETPLACE=1 +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +usage() { + cat <<'U' +GrokHunter Grok Build Connect bootstrap (Termux + desktop) + +Options: + --dir PATH Clone/update location (default: ~/GrokHunter) + --tunnel Also start Cloudflare quick tunnel (tmux session: gbc-tun) + --no-start Do not auto-start MCP in tmux + --start Start MCP in the foreground (blocks; skips tmux) + --no-marketplace Skip grok plugin marketplace add + -h, --help Show help + +Env: + GROKHUNTER_DIR, GROKHUNTER_REPO_URL, GROKHUNTER_BRANCH, GROK_BUILD_CONNECT_PORT +U +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --dir) TARGET_DIR="$2"; shift 2 ;; + --tunnel) START_TUNNEL=1; shift ;; + --no-start) START_MCP=0; shift ;; + --start) START_FOREGROUND=1; START_MCP=0; shift ;; + --no-marketplace) REGISTER_MARKETPLACE=0; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage >&2; exit 2 ;; + esac +done + +need() { + command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; } +} +need git +need python3 +need bash + +SCRIPTS="" +resolve_scripts() { + if [[ -f "$TARGET_DIR/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/skills/grok-build-connect/scripts" + elif [[ -f "$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts/install.sh" ]]; then + SCRIPTS="$TARGET_DIR/plugins/grokhunter-grok-build-connect/skills/grok-build-connect/scripts" + else + return 1 + fi +} + +echo "==> Ensuring GrokHunter at $TARGET_DIR ($BRANCH)" +if [[ -d "$TARGET_DIR/.git" ]]; then + git -C "$TARGET_DIR" fetch --depth 1 origin "$BRANCH" + git -C "$TARGET_DIR" checkout -B "$BRANCH" "origin/$BRANCH" 2>/dev/null \ + || { git -C "$TARGET_DIR" checkout "$BRANCH" 2>/dev/null || true; git -C "$TARGET_DIR" pull --ff-only origin "$BRANCH" || true; } +else + mkdir -p "$(dirname "$TARGET_DIR")" + if [[ -e "$TARGET_DIR" ]] && [[ ! -d "$TARGET_DIR/.git" ]]; then + echo "Path exists but is not a git repo: $TARGET_DIR" >&2 + exit 1 + fi + git clone --depth 1 --branch "$BRANCH" "$REPO_URL" "$TARGET_DIR" +fi + +if ! resolve_scripts; then + echo "grok-build-connect scripts not found in $TARGET_DIR" >&2 + exit 1 +fi + +echo "==> Installing Grok Build Connect MCP" +bash "$SCRIPTS/install.sh" + +echo "==> Ensuring cloudflared" +bash "$SCRIPTS/ensure-cloudflared.sh" || echo "WARN: cloudflared ensure failed" >&2 + +if [[ "$REGISTER_MARKETPLACE" -eq 1 ]] && command -v grok >/dev/null 2>&1; then + grok plugin marketplace add FineComputer14451/GrokHunter 2>/dev/null || true + grok plugin install grokhunter-grok-build-connect --trust 2>/dev/null \ + || grok plugin install "$TARGET_DIR/plugins/grokhunter-grok-build-connect" --trust 2>/dev/null \ + || echo "WARN: plugin install skipped" >&2 +fi + +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" + +if [[ "$START_FOREGROUND" -eq 1 ]]; then + exec "$PREFIX_DIR/start.sh" +fi + +if [[ "$START_MCP" -eq 1 ]]; then + if ! command -v tmux >/dev/null 2>&1; then + echo "tmux missing — start manually: $PREFIX_DIR/start.sh" >&2 + else + tmux has-session -t gbc 2>/dev/null && tmux kill-session -t gbc + echo "==> Starting MCP in tmux session 'gbc'" + tmux new-session -d -s gbc "$PREFIX_DIR/start.sh" + for i in $(seq 1 20); do + curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1 && break + sleep 0.5 + done + if curl -fsS "http://127.0.0.1:${PORT}/healthz"; then + echo + echo "MCP healthy on :$PORT" + else + echo "WARN: MCP healthz not ready — tmux attach -t gbc" >&2 + fi + fi +fi + +if [[ "$START_TUNNEL" -eq 1 ]]; then + if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "Cannot start tunnel — MCP unhealthy" >&2 + exit 1 + fi + if command -v tmux >/dev/null 2>&1; then + tmux has-session -t gbc-tun 2>/dev/null && tmux kill-session -t gbc-tun + echo "==> Starting Cloudflare tunnel in tmux session 'gbc-tun'" + tmux new-session -d -s gbc-tun "bash '$SCRIPTS/tunnel-cloudflare.sh'" + echo "Attach: tmux attach -t gbc-tun" + else + exec bash "$SCRIPTS/tunnel-cloudflare.sh" + fi +fi + +cat </mcp + Token: $PREFIX_DIR/token (do not paste into chat) +EOF diff --git a/skills/grok-build-connect/scripts/ensure-cloudflared.sh b/skills/grok-build-connect/scripts/ensure-cloudflared.sh new file mode 100755 index 0000000..30fda9a --- /dev/null +++ b/skills/grok-build-connect/scripts/ensure-cloudflared.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Ensure cloudflared is on PATH (Termux pkg or GitHub release binary). +set -euo pipefail + +if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 +fi + +BIN_DIR="${TERMUX_CONNECT_BIN:-$HOME/.grokhunter-termux-connect/bin}" +mkdir -p "$BIN_DIR" +TARGET="$BIN_DIR/cloudflared" + +if command -v pkg >/dev/null 2>&1; then + echo "==> Trying: pkg install cloudflared" + if pkg install -y cloudflared 2>/dev/null; then + if command -v cloudflared >/dev/null 2>&1; then + echo "cloudflared: $(command -v cloudflared)" + exit 0 + fi + fi +fi + +arch="$(uname -m)" +case "$arch" in + aarch64|arm64) asset="cloudflared-linux-arm64" ;; + armv7l|armv8l|arm) asset="cloudflared-linux-arm" ;; + x86_64|amd64) asset="cloudflared-linux-amd64" ;; + i686|i386) asset="cloudflared-linux-386" ;; + *) + echo "Unsupported arch for cloudflared binary: $arch" >&2 + echo "Install manually: https://github.com/cloudflare/cloudflared/releases" >&2 + exit 1 + ;; +esac + +url="https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}" +echo "==> Downloading $url" +if command -v curl >/dev/null 2>&1; then + curl -fsSL -o "$TARGET" "$url" +elif command -v wget >/dev/null 2>&1; then + wget -q -O "$TARGET" "$url" +else + echo "Need curl or wget to fetch cloudflared" >&2 + exit 1 +fi +chmod +x "$TARGET" + +# prepend bin dir for this shell / recommend PATH +case ":$PATH:" in + *":$BIN_DIR:"*) ;; + *) export PATH="$BIN_DIR:$PATH" ;; +esac + +# persist hint +profile_snip="$HOME/.grokhunter-termux-connect/path.sh" +cat >"$profile_snip" <

/dev/null; then + echo "[ -f \"$profile_snip\" ] && . \"$profile_snip\"" >>"$HOME/.bashrc" +fi + +if ! command -v cloudflared >/dev/null 2>&1; then + echo "Installed to $TARGET — add to PATH: export PATH=\"$BIN_DIR:\$PATH\"" >&2 + # still usable via absolute path; create shim if ~/bin exists +fi +echo "cloudflared: $TARGET" +echo "CF_BIN=$TARGET" diff --git a/skills/grok-build-connect/scripts/install.sh b/skills/grok-build-connect/scripts/install.sh new file mode 100755 index 0000000..ca14406 --- /dev/null +++ b/skills/grok-build-connect/scripts/install.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Install Grok Build Connect MCP (Termux or desktop). +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PREFIX_DIR="${GROK_BUILD_CONNECT_HOME:-$HOME/.grokhunter-grok-build-connect}" +VENV="$PREFIX_DIR/venv" +TOKEN_FILE="$PREFIX_DIR/token" +ENV_FILE="$PREFIX_DIR/env" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +mkdir -p "$PREFIX_DIR" +python3 -m venv "$VENV" +# shellcheck disable=SC1091 +source "$VENV/bin/activate" +pip install -U pip +pip install -r "$ROOT/requirements.txt" +cp -f "$ROOT/server.py" "$PREFIX_DIR/server.py" +chmod +x "$PREFIX_DIR/server.py" + +if [[ ! -f "$TOKEN_FILE" ]] || [[ "$(wc -c <"$TOKEN_FILE" | tr -d ' ')" -lt 24 ]]; then + python3 -c 'import secrets; print(secrets.token_urlsafe(32))' >"$TOKEN_FILE" + chmod 600 "$TOKEN_FILE" +fi +TOKEN="$(tr -d '[:space:]' <"$TOKEN_FILE")" + +cat >"$ENV_FILE" <"$PREFIX_DIR/start.sh" <<'EOS' +#!/usr/bin/env bash +set -euo pipefail +DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck disable=SC1091 +source "$DIR/venv/bin/activate" +# shellcheck disable=SC1091 +source "$DIR/env" +if ! command -v grok >/dev/null 2>&1; then + echo "WARN: grok not on PATH — install Grok Build before using ask/plugin tools" >&2 +fi +exec python3 "$DIR/server.py" +EOS +chmod +x "$PREFIX_DIR/start.sh" + +cat >"$PREFIX_DIR/print-connector.txt" </mcp + Auth: Bearer (from $TOKEN_FILE — never chat it) + Port: $PORT (default; Termux Connect uses 8765) + +Start: $PREFIX_DIR/start.sh +Tunnel: bash $ROOT/tunnel-cloudflare.sh +Health: curl -s http://127.0.0.1:${PORT}/healthz +EOF + +echo "Installed → $PREFIX_DIR" +echo "Token file (mode 600): $TOKEN_FILE" +echo "Start: $PREFIX_DIR/start.sh" +echo "Works on Termux and desktop. Expose with tunnel-*.sh, register HTTPS URL in Grok Bot." diff --git a/skills/grok-build-connect/scripts/requirements.txt b/skills/grok-build-connect/scripts/requirements.txt new file mode 100644 index 0000000..75f9516 --- /dev/null +++ b/skills/grok-build-connect/scripts/requirements.txt @@ -0,0 +1,2 @@ +mcp>=1.9.0,<2 +uvicorn>=0.30.0 diff --git a/skills/grok-build-connect/scripts/server.py b/skills/grok-build-connect/scripts/server.py new file mode 100755 index 0000000..97233df --- /dev/null +++ b/skills/grok-build-connect/scripts/server.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""GrokHunter Grok Build Connect — HTTP MCP bridging Grok Bot to the grok CLI. + +Works on Termux and desktop (Linux/macOS). Binds 127.0.0.1 by default; +put Cloudflare/ngrok in front. Requires Authorization: Bearer . +""" +from __future__ import annotations + +import os +import re +import secrets +import shutil +import subprocess +import sys +from pathlib import Path + +HOST = os.environ.get("GROK_BUILD_CONNECT_HOST", "127.0.0.1") +PORT = int(os.environ.get("GROK_BUILD_CONNECT_PORT", "8766")) +TOKEN = os.environ.get("GROK_BUILD_CONNECT_TOKEN", "").strip() +HOME = Path(os.environ.get("HOME", str(Path.home()))).resolve() +EXTRA_ROOTS = [ + Path(p).resolve() + for p in os.environ.get("GROK_BUILD_CONNECT_ROOTS", "").split(":") + if p.strip() +] +MAX_BYTES = int(os.environ.get("GROK_BUILD_CONNECT_MAX_BYTES", str(256 * 1024))) +ASK_TIMEOUT = int(os.environ.get("GROK_BUILD_CONNECT_ASK_TIMEOUT", "180")) +ASK_MAX_CHARS = int(os.environ.get("GROK_BUILD_CONNECT_ASK_MAX_CHARS", "8000")) +CWD_DEFAULT = Path(os.environ.get("GROK_BUILD_CONNECT_CWD", str(HOME))).expanduser().resolve() + +# Optional override for grok binary +GROK_BIN = os.environ.get("GROK_BUILD_CONNECT_GROK", "").strip() + + +def _grok() -> str: + if GROK_BIN: + return GROK_BIN + found = shutil.which("grok") + if not found: + raise FileNotFoundError( + "grok CLI not found on PATH. Install Grok Build, then re-run start.sh." + ) + return found + + +def _roots() -> list[Path]: + roots = [HOME, CWD_DEFAULT, *EXTRA_ROOTS] + prefix = os.environ.get("PREFIX") + if prefix: + roots.append(Path(prefix).resolve()) + out: list[Path] = [] + seen = set() + for r in roots: + s = str(r) + if s not in seen: + seen.add(s) + out.append(r) + return out + + +def _jail(path: str) -> Path: + raw = Path(path).expanduser() + if not raw.is_absolute(): + raw = CWD_DEFAULT / raw + resolved = raw.resolve() + for root in _roots(): + try: + resolved.relative_to(root) + return resolved + except ValueError: + continue + raise PermissionError(f"path outside jail: {path}") + + +def _check_token() -> None: + if not TOKEN or len(TOKEN) < 24: + print( + "GROK_BUILD_CONNECT_TOKEN missing or too short (min 24 chars).\n" + 'Generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))"', + file=sys.stderr, + ) + sys.exit(2) + + +def _run(argv: list[str], timeout: int | None = None, cwd: Path | None = None) -> str: + try: + proc = subprocess.run( + argv, + capture_output=True, + text=True, + timeout=timeout or 90, + cwd=str(cwd or CWD_DEFAULT), + env={**os.environ, "TERM": os.environ.get("TERM", "xterm-256color"), "CI": "1"}, + ) + except FileNotFoundError as e: + return f"not found: {e}" + except subprocess.TimeoutExpired: + return f"timeout after {timeout or 90}s" + out = (proc.stdout or "") + (("\n" + proc.stderr) if proc.stderr else "") + raw = out.encode("utf-8", errors="replace") + if len(raw) > MAX_BYTES: + out = raw[:MAX_BYTES].decode("utf-8", errors="replace") + "\n…[truncated]" + return f"exit {proc.returncode}\n{out}".rstrip() + + +def main() -> None: + _check_token() + try: + from mcp.server.fastmcp import FastMCP + from mcp.server.transport_security import TransportSecuritySettings + from starlette.middleware.base import BaseHTTPMiddleware + from starlette.requests import Request + from starlette.responses import JSONResponse + from starlette.routing import Route + import uvicorn + except ImportError as e: + print("Missing deps. Run: pip install -r requirements.txt\n" + str(e), file=sys.stderr) + sys.exit(1) + + # Cloudflare quick tunnels send Host: *.trycloudflare.com (see termux-connect fix). + transport_security = TransportSecuritySettings(enable_dns_rebinding_protection=False) + extra_hosts = [ + h.strip() + for h in os.environ.get("GROK_BUILD_CONNECT_ALLOWED_HOSTS", "").split(",") + if h.strip() + ] + if extra_hosts: + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=[ + f"{HOST}:{PORT}", + HOST, + "localhost", + f"localhost:{PORT}", + "127.0.0.1:*", + "localhost:*", + *extra_hosts, + ], + ) + + mcp = FastMCP( + "grokhunter-grok-build-connect", + instructions=( + "Scoped tools for the local Grok Build (`grok`) CLI on Termux or desktop. " + "Prefer grok_version / grok_plugin_list / grok_ask. Paths are jailed to HOME." + ), + host=HOST, + port=PORT, + streamable_http_path="/mcp", + stateless_http=True, + transport_security=transport_security, + ) + + @mcp.tool() + def grok_version() -> str: + """Return `grok --version` and resolved binary path.""" + g = _grok() + return f"binary={g}\n" + _run([g, "--version"], timeout=30) + + @mcp.tool() + def grok_plugin_list() -> str: + """List installed Grok Build plugins (`grok plugin list`).""" + return _run([_grok(), "plugin", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_list() -> str: + """List MCP servers configured in Grok Build (`grok mcp list`).""" + return _run([_grok(), "mcp", "list"], timeout=60) + + @mcp.tool() + def grok_mcp_doctor() -> str: + """Diagnose Grok Build MCP config (`grok mcp doctor`).""" + return _run([_grok(), "mcp", "doctor"], timeout=90) + + @mcp.tool() + def grok_ask(prompt: str, model: str = "", cwd: str = "") -> str: + """Single-turn headless `grok -p` (prints response and exits). Caps prompt length.""" + p = (prompt or "").strip() + if not p: + return "empty prompt" + if len(p) > ASK_MAX_CHARS: + return f"prompt too long (max {ASK_MAX_CHARS} chars)" + work = _jail(cwd) if cwd.strip() else CWD_DEFAULT + if not work.is_dir(): + return f"cwd not a directory: {work}" + argv = [_grok(), "-p", p, "--output-format", "text"] + if model.strip(): + argv.extend(["-m", model.strip()]) + return _run(argv, timeout=ASK_TIMEOUT, cwd=work) + + @mcp.tool() + def list_dir(path: str = ".") -> str: + """List a directory under the jail (HOME / PREFIX / configured cwd).""" + target = _jail(path) + if not target.is_dir(): + return f"not a directory: {target}" + lines = [] + for child in sorted(target.iterdir(), key=lambda p: p.name.lower())[:500]: + kind = "d" if child.is_dir() else "f" + lines.append(f"{kind}\t{child.name}") + return "\n".join(lines) or "(empty)" + + @mcp.tool() + def read_file(path: str, max_bytes: int = 65536) -> str: + """Read a text file under the jail (capped).""" + target = _jail(path) + if not target.is_file(): + return f"not a file: {target}" + n = min(max(1, max_bytes), MAX_BYTES) + data = target.read_bytes()[: n + 1] + truncated = len(data) > n + text = data[:n].decode("utf-8", errors="replace") + if truncated: + text += "\n…[truncated]" + return text + + class BearerAuth(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.url.path in {"/healthz", "/"}: + return await call_next(request) + auth = request.headers.get("authorization", "") + expected = f"Bearer {TOKEN}" + if not secrets.compare_digest(auth, expected): + return JSONResponse({"error": "unauthorized"}, status_code=401) + return await call_next(request) + + async def healthz(request: Request): + grok_ok = False + try: + _grok() + grok_ok = True + except Exception: + grok_ok = False + return JSONResponse( + { + "ok": True, + "service": "grokhunter-grok-build-connect", + "grok_on_path": grok_ok, + "port": PORT, + } + ) + + app = mcp.streamable_http_app() + app.add_middleware(BearerAuth) + app.routes.insert(0, Route("/healthz", healthz, methods=["GET"])) + + print( + f"grok-build-connect listening on http://{HOST}:{PORT}/mcp " + f"(cwd={CWD_DEFAULT}, jail={_roots()})", + flush=True, + ) + uvicorn.run(app, host=HOST, port=PORT, log_level="info") + + +if __name__ == "__main__": + main() diff --git a/skills/grok-build-connect/scripts/tunnel-cloudflare.sh b/skills/grok-build-connect/scripts/tunnel-cloudflare.sh new file mode 100755 index 0000000..5c1a602 --- /dev/null +++ b/skills/grok-build-connect/scripts/tunnel-cloudflare.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Cloudflare quick tunnel in front of Grok Build Connect MCP. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" + +ensure_log="$(mktemp)" +bash "$ROOT/ensure-cloudflared.sh" | tee "$ensure_log" +CF="" +if command -v cloudflared >/dev/null 2>&1; then + CF="$(command -v cloudflared)" +elif [[ -x "$HOME/.grokhunter-grok-build-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-grok-build-connect/bin/cloudflared" +elif [[ -x "$HOME/.grokhunter-termux-connect/bin/cloudflared" ]]; then + CF="$HOME/.grokhunter-termux-connect/bin/cloudflared" +else + CF="$(grep '^CF_BIN=' "$ensure_log" | tail -n1 | cut -d= -f2- || true)" +fi +rm -f "$ensure_log" + +if [[ -z "${CF:-}" || ! -x "$CF" ]]; then + echo "cloudflared still missing" >&2 + exit 1 +fi + +for i in $(seq 1 15); do + if curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + break + fi + sleep 1 +done +if ! curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1; then + echo "MCP not healthy on :${PORT} — start it first (tmux attach -t gbc)" >&2 + exit 1 +fi + +echo "Starting quick tunnel → http://127.0.0.1:${PORT}" +echo "Copy the https://….trycloudflare.com URL, append /mcp" +echo "Bearer token file: ~/.grokhunter-grok-build-connect/token (do not paste into chat)" +exec "$CF" tunnel --url "http://127.0.0.1:${PORT}" diff --git a/skills/grok-build-connect/scripts/tunnel-ngrok.sh b/skills/grok-build-connect/scripts/tunnel-ngrok.sh new file mode 100755 index 0000000..9e88937 --- /dev/null +++ b/skills/grok-build-connect/scripts/tunnel-ngrok.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +PORT="${GROK_BUILD_CONNECT_PORT:-8766}" +if ! command -v ngrok >/dev/null 2>&1; then + echo "Install ngrok or use tunnel-cloudflare.sh" + exit 1 +fi +echo "Starting ngrok → http://127.0.0.1:${PORT}" +echo "Copy the https URL, append /mcp, Bearer from ~/.grokhunter-grok-build-connect/token" +exec ngrok http "$PORT"