Merges a Clojure function and its transitive dependencies into one standalone,
single-segment namespace with no :require and no (require ...) — a single
file you can load-file anywhere bare Clojure runs.
clojure -M:run -d /path/to/project my.app.core/process
# => out/process.clj with (ns process) and everything `process` needs, inlined
clojure -M:run [opts] <ns/fn>
-o, --out FILE Output file (default out/<fn-name>.clj)
-n, --out-ns NAME Output namespace (single segment; default fn name)
-p, --path PATH Source path relative to project dir (repeatable; default src)
-d, --project-dir DIR Project root of the entry fn (default .)
--full-classpath Lint the project's full classpath — inlines library fns too
--inline-preloaded Inline clojure.string/walk/edn/java.io instead of relying on boot preload
--force-include VAR Extra ns/var to include (repeatable; for dynamic loading the
static analysis can't see)
--[no-]verify Sandbox load check (default on)
-l, --llm LLM-assisted mode (see below)
-m, --llm-model MODEL Model passed to `llm -m`
--max-fix-iters N Max LLM iterations per loop (default 4)
Exit 0 when nothing is unresolved and the sandbox load check passes. The report (EDN) lists renames, dynamic rewrites, unresolved vars, and warnings.
- Analysis — clj-kondo over the project paths plus the Clojure jar
(
--full-classpathto cover third-party libraries; their sources are read straight out of the jars). - Closure — transitive var-usage walk from the entry fn.
defmethodandextend-protocol/extend-typeforms ride along with theirdefmulti/defprotocol. Only vars actually reachable are emitted — unused siblings never appear. - Dynamic loading — literal
(requiring-resolve 'ns/fn)/(resolve 'ns/fn)calls are detected: the target is pulled into the closure and the call site is rewritten to a direct#'fnreference. Non-literal dynamic loads produce a warning; combine--force-include(static) or--llm(sandbox probing) to resolve them. - Rewrite — collisions renamed (
<ns-segment>-<name>; names that clash with clojure.core are always renamed), aliased/refered symbols fully qualified,::kw/::alias/kwauto-resolved keywords frozen to their original namespace,:importlist rebuilt from classes actually used. - Order — topological sort of forms; mutual recursion gets a
(declare ...)block, macros are ordered before their uses. - Verify — the bundle is
load-filed in.no-require/sandbox/against bare Clojure (-Srepro, no deps). That check is the no-require guarantee.
Namespaces preloaded by clojure.main boot (clojure.string, clojure.walk,
clojure.edn, clojure.java.io, verified against 1.12) stay fully-qualified
instead of inlined — shorter output, still loads without :require. Use
--inline-preloaded if the bundle must survive exotic boot paths (AOT, Graal).
Drives the llm CLI with a restricted toolbox
(read_file, write_file, read_project_file, list_project_files,
run_clojure — all confined to the sandbox / read-only project) via
llm --functions:
- Fix loop — if the sandbox load fails (e.g. input-dependent dynamic requires), the LLM reads the error, inlines what's missing from the original sources, and re-runs until the bundle loads. Successful fixes are synced back to the output file.
- Tests — writes
bundle_test.cljin the sandbox: clojure.test unit tests for the entry fn, reusing existing project tests when found; aclojure.alpha.spec(spec-alpha2)fdef; and, when the specs are generatable, a test.checkdefspecproperty — the automatically derivable one being equivalence:(bundled x) = (original x)for generated inputs (the original project is on the sandbox classpath under the:origalias). - Test failures loop back through
llm -cuntil green or--max-fix-iters.
.clj/ JVM only (no cljs, no reader conditionals).- AOT-only dependencies (no source on the classpath) and Java class dependencies
from libraries can't be inlined — they surface as
:unresolved. (require ...)with computed arguments,eval,internare reported, not solved statically; use--force-includeor--llm. A surviving non-literalresolve/requiring-resolvealso fails the:forbiddenartifact scan.- Renaming a colliding
defrecord/deftypeconstructor is not supported (the record keeps its name; collisions between record names abort at load). defmethod/extend-*on clojure.core or preloaded targets (print-method,Datafiable, ...) ride along only when the form mentions an inlined record/type; keyword-dispatch or java-class-only forms are dropped silently.- Top-level side effects other than var definitions,
defmethod, andextend-*forms (e.g. bare(swap! registry ...)calls,s/defregistrations) are not carried over — they are reported as:side-effect-form-skippedwarnings. - "Preloaded" namespaces assume the bundle is loaded via clojure.main boot.
clojure -M:test # unit + fixture E2E (generates, then runs the bundle against bare Clojure)