From 1c53778a365f2483121fe42b95978b675cff019c Mon Sep 17 00:00:00 2001 From: Daveswo <969dwi@gmail.com> Date: Tue, 18 Aug 2026 00:09:54 -0400 Subject: [PATCH] Add 70-local-llm.sh: last-resort fix source querying a local Ollama/LM Studio server --- contrib/fx/sources.d/70-local-llm.sh | 106 +++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100755 contrib/fx/sources.d/70-local-llm.sh diff --git a/contrib/fx/sources.d/70-local-llm.sh b/contrib/fx/sources.d/70-local-llm.sh new file mode 100755 index 0000000..7035b11 --- /dev/null +++ b/contrib/fx/sources.d/70-local-llm.sh @@ -0,0 +1,106 @@ +#!/bin/sh +# Last-resort source: queries a local LLM inference server (Ollama or +# LM Studio, both common on dev machines) for a fix, only when nothing +# cheaper matched. Silent no-op if no local server is reachable -- this +# never touches the network, never calls a paid API, and costs nothing +# when you don't have a local model running. +# +# Numbered 70, after every deterministic/heuristic source (10-50) and +# before the team-shared source (90): cheap, verified candidates should +# always win first. fx.sh's FX_AUTO picks the *first* candidate in +# source order, so this only gets auto-selected when it's the only +# opinion offered at all -- still review it, this is a guess, not a +# fact. Every other default source is either a cached human decision or +# a deterministic rule; this is the one source capable of confidently +# suggesting something wrong. +# +# Needs python3 (uses urllib, no extra dependency beyond stdlib) -- +# silently no-ops without it rather than ship a fragile hand-rolled +# JSON encoder in shell. + +cmd="$1" +[ -n "$cmd" ] || exit 0 +command -v python3 >/dev/null 2>&1 || exit 0 + +OLLAMA_URL="${FX_LLM_OLLAMA_URL:-http://127.0.0.1:11434}" +LMSTUDIO_URL="${FX_LLM_LMSTUDIO_URL:-http://127.0.0.1:1234}" + +python3 - "$cmd" "$FX_OUTPUT" "$OLLAMA_URL" "$LMSTUDIO_URL" <<'PYEOF' +import sys, json, urllib.request, urllib.error + +cmd, output, ollama_url, lmstudio_url = sys.argv[1:5] + +PROMPT = f"""You are suggesting a fix for a failed shell command, to be taught to a self-healing shell function called f(). +Rules: +- f() will eval your fix ONCE as a precondition, then re-run the ORIGINAL command verbatim. Your fix never replaces the command; it only sets up state so the original command then succeeds. +- If the fix should generalize beyond this one invocation, define a shell function with the same name as the failing program, wrapping the real binary via `command "$@"`, and only special-case the failure inside it. +- Respond with EXACTLY ONE LINE of POSIX shell and nothing else: no explanation, no markdown, no code fences, no commentary. + +Failing command: {cmd} + +Its output: +{output} +""" + +def http_json(url, payload, timeout): + req = urllib.request.Request( + url, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}, method="POST", + ) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + +def http_get_json(url, timeout): + with urllib.request.urlopen(url, timeout=timeout) as r: + return json.loads(r.read().decode()) + +def try_ollama(): + tags = http_get_json(ollama_url.rstrip("/") + "/api/tags", 2) + models = tags.get("models") or [] + if not models: + return None + model = models[0].get("name") + if not model: + return None + resp = http_json( + ollama_url.rstrip("/") + "/api/generate", + {"model": model, "prompt": PROMPT, "stream": False}, + 25, + ) + return (resp.get("response") or "").strip() + +def try_lmstudio(): + models = http_get_json(lmstudio_url.rstrip("/") + "/v1/models", 2) + data = models.get("data") or [] + if not data: + return None + model = data[0].get("id") + if not model: + return None + resp = http_json( + lmstudio_url.rstrip("/") + "/v1/chat/completions", + { + "model": model, + "messages": [{"role": "user", "content": PROMPT}], + "temperature": 0, + }, + 25, + ) + choices = resp.get("choices") or [] + if not choices: + return None + return (choices[0].get("message", {}).get("content") or "").strip() + +for attempt in (try_ollama, try_lmstudio): + try: + result = attempt() + except (urllib.error.URLError, TimeoutError, OSError, ValueError, KeyError): + result = None + if result: + # Enforce the one-line contract even if the model ignores instructions. + first_line = result.splitlines()[0].strip() + first_line = first_line.strip("`") + if first_line: + print(first_line) + break +PYEOF