vis-language-clojure — Clojure language handlers for Vis.
Format/test/REPL are exposed through the generic language facade
(format, test, repl_eval, repl_start, repl_stop) —
format here does parinfer delimiter repair + cljfmt. The pack also registers
a cross-cutting op-hook that auto repairs+formats .clj files after the
foundation's struct_patch / patch / write, so no separate repair step is
needed.
vis-language-clojure — Clojure language handlers for Vis. Format/test/REPL are exposed through the generic language facade (`format`, `test`, `repl_eval`, `repl_start`, `repl_stop`) — `format` here does parinfer delimiter repair + cljfmt. The pack also registers a cross-cutting op-hook that auto repairs+formats `.clj` files after the foundation's struct_patch / patch / write, so no separate repair step is needed.
Config-driven Clojure source formatter used by clj/edit for format-on-write
and by the format_code language-surface verb.
TWO backends live here, and the choice is TRANSPARENT to the language surface — callers just format; this namespace picks the formatter from the config files present around the target path:
.zprint.edn/.zprintrc is found walking UP from the
path. The project's zprint options map is applied. This is the
canonical, reflowing formatter (matches the repo's :format
alias / codestyle)..cljfmt.edn/.cljfmt.clj is found (no zprint), or
when neither config exists (cljfmt defaults). Conservative:
normalizes indentation + whitespace of MULTI-LINE forms but does
NOT reflow a one-liner into multiple lines.When BOTH configs are present, zprint WINS.
Failure mode: if a backend refuses (parse error, unfamiliar reader macro, anything that throws), the formatter returns the original source unchanged. We never silently corrupt a file because the formatter choked.
Config-driven Clojure source formatter used by `clj/edit` for format-on-write
and by the `format_code` language-surface verb.
TWO backends live here, and the choice is TRANSPARENT to the language
surface — callers just format; this namespace picks the formatter from the
config files present around the target path:
* zprint — when a `.zprint.edn`/`.zprintrc` is found walking UP from the
path. The project's zprint options map is applied. This is the
canonical, reflowing formatter (matches the repo's `:format`
alias / codestyle).
* cljfmt — when only a `.cljfmt.edn`/`.cljfmt.clj` is found (no zprint), or
when neither config exists (cljfmt defaults). Conservative:
normalizes indentation + whitespace of MULTI-LINE forms but does
NOT reflow a one-liner into multiple lines.
When BOTH configs are present, zprint WINS.
Failure mode: if a backend refuses (parse error, unfamiliar reader macro,
anything that throws), the formatter returns the original source unchanged.
We never silently corrupt a file because the formatter choked.clj-kondo linting for the Vis language surface.
Runs clj-kondo's programmatic API (clj-kondo.core/run!) — never shells out —
over a code string (fed on stdin as -), explicit path(s), or the workspace's
default source paths, and returns a uniform result map (STRING keys — crosses
the strings-only boundary as a tool :result):
{"op" "clj-lint" "error" N "warning" N "info" N "files" N "findings" [...]}
where each finding is {"file" "row" "col" "level" "type" "message"}.
clj-kondo linting for the Vis language surface.
Runs clj-kondo's programmatic API (`clj-kondo.core/run!`) — never shells out —
over a code string (fed on stdin as `-`), explicit path(s), or the workspace's
default source paths, and returns a uniform result map (STRING keys — crosses
the strings-only boundary as a tool `:result`):
`{"op" "clj-lint" "error" N "warning" N "info" N "files" N "findings" [...]}`
where each finding is `{"file" "row" "col" "level" "type" "message"}`.Thin, observable nREPL client for clj/eval.
Connection model:
nrepl.core/connect socket per [host port] key, cached
on a defonce atom so we survive (require :reload) during
development.eval! we open a fresh client-session against the
cached connection. Sessions are not shared across calls so a
caller's *1/*e/dynamic var binding never leaks across
Vis tool invocations.IOException / nil
message stream) and the entry is evicted; the next call
re-dials.Returned shape (success) — STRING keys (crosses the strings-only boundary
as a tool :result; enrichment adds "error_message"/"error_data"/"trace"):
{"value" "42" ; pr-str of the LAST form's value, or nil
"values" ["1" "42"] ; pr-str of every emitted value
"out" "hello\n" ; stdout aggregated
"err" "" ; stderr aggregated
"ns" "user" ; final ns name
"status" #{"done"} ; nREPL status set (strings)
"ex" nil ; exception class name, when status :ex
"root_ex" nil ; root exception class name
"ms" 12 ; wall-clock duration
"port" 7888
"timed_out" false}
Failure paths throw ex-info with :type :clj/nrepl-* so the
Vis tool wrapper can surface a clean error to the model.
Thin, observable nREPL client for `clj/eval`.
Connection model:
* One `nrepl.core/connect` socket per `[host port]` key, cached
on a `defonce` atom so we survive `(require :reload)` during
development.
* On every `eval!` we open a fresh `client-session` against the
cached connection. Sessions are *not* shared across calls so a
caller's `*1`/`*e`/dynamic var binding never leaks across
Vis tool invocations.
* Stale / closed sockets are detected (`IOException` / `nil`
message stream) and the entry is evicted; the next call
re-dials.
Returned shape (success) — STRING keys (crosses the strings-only boundary
as a tool `:result`; enrichment adds "error_message"/"error_data"/"trace"):
{"value" "42" ; pr-str of the LAST form's value, or nil
"values" ["1" "42"] ; pr-str of every emitted value
"out" "hello\n" ; stdout aggregated
"err" "" ; stderr aggregated
"ns" "user" ; final *ns* name
"status" #{"done"} ; nREPL status set (strings)
"ex" nil ; exception class name, when status :ex
"root_ex" nil ; root exception class name
"ms" 12 ; wall-clock duration
"port" 7888
"timed_out" false}
Failure paths throw `ex-info` with `:type :clj/nrepl-*` so the
Vis tool wrapper can surface a clean error to the model.Per-turn :ext/ctx-fn contribution for the Clojure pack.
Instead of forcing the model to call clj_repl() over and over, the engine
injects live nREPL state into context as standing knowledge, nested UNDER the
active language so a polyglot repo accumulates
:languages {:clojure {...} :typescript {...}}:
{"session_env" {"languages" {"clojure" {"nrepl" {"default" <id|nil> "repls" [{"id" "dir" "port" "aliases" "tool" "status" "managed" ["dialect" "versions"]} ...]}}}}}
OWNERSHIP: we surface ONLY the REPLs THIS session started + owns (from
repl-manager/session-repls). There is no external-port discovery and no
.nrepl-port scanning — a REPL vis did not start is not vis's to show or stop.
The default is the id of the SINGLE owned REPL (nil when zero or many): with
one REPL that id is the implicit eval target; with several, eval still resolves
WITHOUT the model naming an id — it defaults to the workspace-root REPL (else the
first) and the eval result reports which REPL ran under its repl field, so the
model can pass an explicit id to override. Each REPL is mirrored into the
session resource registry (footer badge
status from a per-turn probe.All best-effort: any failure degrades to an empty contribution and never blocks the render.
Per-turn `:ext/ctx-fn` contribution for the Clojure pack.
Instead of forcing the model to call `clj_repl()` over and over, the engine
injects live nREPL state into context as standing knowledge, nested UNDER the
active language so a polyglot repo accumulates
`:languages {:clojure {...} :typescript {...}}`:
{"session_env" {"languages" {"clojure"
{"nrepl" {"default" <id|nil>
"repls" [{"id" "dir" "port" "aliases" "tool"
"status" "managed" ["dialect" "versions"]} ...]}}}}}
OWNERSHIP: we surface ONLY the REPLs THIS session started + owns (from
`repl-manager/session-repls`). There is no external-port discovery and no
`.nrepl-port` scanning — a REPL vis did not start is not vis's to show or stop.
The `default` is the id of the SINGLE owned REPL (nil when zero or many): with
one REPL that id is the implicit eval target; with several, eval still resolves
WITHOUT the model naming an id — it defaults to the workspace-root REPL (else the
first) and the eval result reports which REPL ran under its `repl` field, so the
model can pass an explicit `id` to override. Each REPL is mirrored into the
session resource registry (footer badge
+ F4 stop/restart) and carries a liveness `status` from a per-turn probe.
All best-effort: any failure degrades to an empty contribution and never
blocks the render.Pure-Clojure delimiter repair for Clojure source the model hand-wrote.
Ported from bhauman/clojure-mcp-light (clojure-mcp-light.delimiter-repair,
Apache-2.0): detect a real delimiter error with edamame, then repair via
parinferish indent-mode — parinfer trusts the INDENTATION to place the missing
/ extra ( [ {, which matches how the model intended the code to nest. The
parinfer-rust shell path + stats/json bits from upstream are dropped; this is
the pure JVM path only.
fix-delimiters is the entry point used by the clj_paren_repair tool.
Pure-Clojure delimiter repair for Clojure source the model hand-wrote.
Ported from bhauman/clojure-mcp-light (`clojure-mcp-light.delimiter-repair`,
Apache-2.0): detect a real delimiter error with edamame, then repair via
parinferish indent-mode — parinfer trusts the INDENTATION to place the missing
/ extra `( [ {`, which matches how the model intended the code to nest. The
parinfer-rust shell path + stats/json bits from upstream are dropped; this is
the pure JVM path only.
`fix-delimiters` is the entry point used by the `clj_paren_repair` tool.Owned, session-scoped nREPL lifecycle for the Clojure pack.
OWNERSHIP: each vis SESSION owns its own nREPL subprocess(es). The processes
atom is keyed by [session-id dir], so two sessions in the same directory get
two independent REPLs and neither can see or stop the other's. A managed REPL
lives and dies with THIS vis process — there is NO persistent registry and NO
PID re-attach across a vis restart. Restarting vis means a fresh REPL, exactly
like the Python pack.
PORT: we PICK a free ephemeral port ourselves and pass it to the launcher
EXPLICITLY (nrepl.cmdline --port N, lein repl :headless :port N,
bb nrepl-server N), so we always KNOW our port without ever reading a
.nrepl-port file back. Any stray .nrepl-port a tool drops in the project is
deleted after boot — vis never depends on it and never leaves it behind.
ALIASES: a REPL is ALWAYS booted with the project's :dev :test deps + paths
on its classpath (full dependency spec), with the user's :main-opts dropped
(our synthetic :vis/nrepl-launch alias appends last so -m nrepl.cmdline
wins). Unknown :dev/:test aliases are silently ignored by tools.deps, so
this is safe in any project.
Starting/stopping is CORE and ALWAYS allowed — never gated behind a flag.
Owned, session-scoped nREPL lifecycle for the Clojure pack. OWNERSHIP: each vis SESSION owns its own nREPL subprocess(es). The `processes` atom is keyed by `[session-id dir]`, so two sessions in the same directory get two independent REPLs and neither can see or stop the other's. A managed REPL lives and dies with THIS vis process — there is NO persistent registry and NO PID re-attach across a vis restart. Restarting vis means a fresh REPL, exactly like the Python pack. PORT: we PICK a free ephemeral port ourselves and pass it to the launcher EXPLICITLY (`nrepl.cmdline --port N`, `lein repl :headless :port N`, `bb nrepl-server N`), so we always KNOW our port without ever reading a `.nrepl-port` file back. Any stray `.nrepl-port` a tool drops in the project is deleted after boot — vis never depends on it and never leaves it behind. ALIASES: a REPL is ALWAYS booted with the project's `:dev :test` deps + paths on its classpath (full dependency spec), with the user's `:main-opts` dropped (our synthetic `:vis/nrepl-launch` alias appends last so `-m nrepl.cmdline` wins). Unknown `:dev`/`:test` aliases are silently ignored by tools.deps, so this is safe in any project. Starting/stopping is CORE and ALWAYS allowed — never gated behind a flag.
Run a namespace's tests over the live nREPL (the fast inner loop) or, when no nREPL is reachable, by shelling clojure -M:test (the suite gate).
The in-REPL path is FRAMEWORK-AGNOSTIC: a ns whose vars carry clojure.test :test metadata runs through clojure.test/run-tests; otherwise it is treated as lazytest and run through lazytest.runner/run-tests. Either way the result is a uniform STRING-keyed map (crosses the strings-only boundary) with "mode" (repl or cli), "framework", "ns", "total", "pass", "fail" and "failures" [{"ns" "test" "message" "file" "line"} ...].
run-form is the code EVALED on the target nREPL. It is a quoted form (not a call into this namespace) so it works against ANY project's nREPL, including hosts that do not have the vis extension on their classpath.
Run a namespace's tests over the live nREPL (the fast inner loop) or, when no
nREPL is reachable, by shelling clojure -M:test (the suite gate).
The in-REPL path is FRAMEWORK-AGNOSTIC: a ns whose vars carry clojure.test
:test metadata runs through clojure.test/run-tests; otherwise it is treated
as lazytest and run through lazytest.runner/run-tests. Either way the result
is a uniform STRING-keyed map (crosses the strings-only boundary) with
"mode" (repl or cli), "framework", "ns", "total", "pass", "fail" and
"failures" [{"ns" "test" "message" "file" "line"} ...].
run-form is the code EVALED on the target nREPL. It is a quoted form (not a
call into this namespace) so it works against ANY project's nREPL, including
hosts that do not have the vis extension on their classpath.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |