Created
May 27, 2026 12:41
-
-
Save maxweber/4b03dff8b3ab3fed7f42891e60faada6 to your computer and use it in GitHub Desktop.
Evaluate Clojure code in the running app (written by Claude)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bb | |
| ;; ============================================================================= | |
| ;; eval-app - Evaluate Clojure code in the running app | |
| ;; ============================================================================= | |
| ;; | |
| ;; This tool connects to the nREPL of the app running evaluates Clojure code. | |
| ;; Useful for inspecting app state, querying the database, or debugging issues | |
| ;; in the running system. | |
| ;; | |
| ;; USAGE: | |
| ;; cat <<'EOF' | agent-tools/eval-app | |
| ;; (your-code-here) | |
| ;; EOF | |
| ;; | |
| ;; cat code.clj | agent-tools/eval-app | |
| ;; | |
| ;; OPTIONS: | |
| ;; --host HOST nREPL host (default: localhost) | |
| ;; --port PORT nREPL port (default: 4000) | |
| ;; --timeout MS Timeout in milliseconds (default: 30000) | |
| ;; --help, -h Show help | |
| ;; | |
| ;; EXAMPLES: | |
| ;; | |
| ;; # Get the current database value | |
| ;; cat <<'EOF' | agent-tools/eval-app | |
| ;; (dev/db) | |
| ;; EOF | |
| ;; | |
| ;; # Query an entity | |
| ;; cat <<'EOF' | agent-tools/eval-app | |
| ;; (dev/show-entity 12345) | |
| ;; EOF | |
| ;; | |
| ;; # Check system state | |
| ;; cat <<'EOF' | agent-tools/eval-app | |
| ;; (keys @app.system/state) | |
| ;; EOF | |
| ;; | |
| ;; # Multi-line code | |
| ;; cat <<'EOF' | agent-tools/eval-app | |
| ;; (d/q '[:find ?e :where [?e :user/email]] (dev/db)) | |
| ;; EOF | |
| ;; | |
| ;; OUTPUT: | |
| ;; Returns the result of evaluating the Clojure code, pretty-printed. | |
| ;; Errors are printed to stderr. | |
| ;; | |
| ;; ============================================================================= | |
| (require '[babashka.cli :as cli] | |
| '[clojure.string :as str] | |
| '[bencode.core :as bencode]) | |
| (import '[java.net Socket] | |
| '[java.io PushbackInputStream]) | |
| (def cli-spec | |
| {:host {:desc "nREPL host" | |
| :default "localhost"} | |
| :port {:desc "nREPL port" | |
| :default 4000 | |
| :coerce :int} | |
| :timeout {:desc "Timeout in milliseconds" | |
| :default 30000 | |
| :coerce :int} | |
| :help {:desc "Show help" | |
| :alias :h}}) | |
| (defn print-help [] | |
| (println "eval-app - Evaluate Clojure code in the running app") | |
| (println) | |
| (println "Usage: cat <<'EOF' | agent-tools/eval-app [OPTIONS]") | |
| (println " (your-code-here)") | |
| (println " EOF") | |
| (println) | |
| (println " cat code.clj | agent-tools/eval-app [OPTIONS]") | |
| (println) | |
| (println "Options:") | |
| (println " --host HOST nREPL host (default: localhost)") | |
| (println " --port PORT nREPL port (default: 4000)") | |
| (println " --timeout MS Timeout in milliseconds (default: 30000)") | |
| (println " --help, -h Show this help") | |
| (println) | |
| (println "Examples:") | |
| (println " cat <<'EOF' | agent-tools/eval-app") | |
| (println " (dev/db)") | |
| (println " EOF")) | |
| (defn bytes->str [x] | |
| (if (bytes? x) | |
| (String. ^bytes x "UTF-8") | |
| x)) | |
| (defn parse-response [response] | |
| (into {} | |
| (map (fn [[k v]] | |
| [(keyword (bytes->str k)) | |
| (if (sequential? v) | |
| (mapv bytes->str v) | |
| (bytes->str v))]) | |
| response))) | |
| (defn nrepl-eval [{:keys [host port timeout]} code] | |
| (let [socket (Socket. ^String host ^int port) | |
| _ (.setSoTimeout socket timeout) | |
| out (.getOutputStream socket) | |
| in (PushbackInputStream. (.getInputStream socket)) | |
| session-id (str (random-uuid)) | |
| msg {"op" "eval" | |
| "code" code | |
| "id" session-id}] | |
| (try | |
| (bencode/write-bencode out msg) | |
| (loop [has-error false] | |
| (let [response (parse-response (bencode/read-bencode in))] | |
| (when-let [out-str (:out response)] | |
| (print out-str) | |
| (flush)) | |
| (when-let [err-str (:err response)] | |
| (binding [*out* *err*] | |
| (print err-str) | |
| (flush))) | |
| (when-let [value (:value response)] | |
| (println value)) | |
| (when-let [ex (:ex response)] | |
| (binding [*out* *err*] | |
| (println "Exception:" ex))) | |
| (let [status (set (:status response)) | |
| has-error (or has-error | |
| (contains? status "error") | |
| (contains? status "eval-error"))] | |
| (if (contains? status "done") | |
| (if has-error 1 0) | |
| (recur has-error))))) | |
| (finally | |
| (.close socket))))) | |
| (defn eval-code [opts code] | |
| (try | |
| (nrepl-eval opts code) | |
| (catch java.net.ConnectException e | |
| (binding [*out* *err*] | |
| (println "Error: Cannot connect to nREPL at" (str (:host opts) ":" (:port opts))) | |
| (println "Make sure the app is running (bin/dev up)")) | |
| 1) | |
| (catch java.net.SocketTimeoutException e | |
| (binding [*out* *err*] | |
| (println "Error: nREPL request timed out")) | |
| 1) | |
| (catch Exception e | |
| (binding [*out* *err*] | |
| (println "Error:" (.getMessage e))) | |
| 1))) | |
| (let [{:keys [opts]} (cli/parse-args *command-line-args* {:spec cli-spec}) | |
| opts-with-defaults (merge {:host "localhost" :port 4000 :timeout 30000} opts)] | |
| (if (:help opts) | |
| (print-help) | |
| (let [code (slurp *in*)] | |
| (if (str/blank? code) | |
| (do (binding [*out* *err*] | |
| (println "Error: No code provided on stdin") | |
| (println "Usage: cat <<'EOF' | agent-tools/eval-app") | |
| (println " (dev/db)") | |
| (println " EOF") | |
| (println "Run with --help for more information")) | |
| (System/exit 1)) | |
| (System/exit (eval-code opts-with-defaults code)))))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment