Julia’s startup + compilation tax is brutal when an AI assistant (or a shell loop, or CI) spawns a fresh julia on every call. Inspired by julia-mcp, julia-client solves the same problem, but as an ordinary command-line tool — one static binary, usable by humans, scripts, and any agent.
What it is
A single Go binary that is both client and daemon. First eval auto-starts a background daemon over a Unix socket; it holds long-lived julia -i --project=<dir> processes so variables, loaded packages, and session state survive between calls. Idle daemon shuts itself down after 1h.
curl -fsSL https://raw.githubusercontent.com/Beforerr/julia-client/main/install.sh | bash
julia-client --project=@temp -e 'using Pkg; Pkg.add("DataFrames"); using DataFrames' # daemon starts; pkg loaded
julia-client --project=@temp -E 'df = DataFrame(a=1:3)' # -E displays the result
julia-client --project=@temp -e 'sum(df.a)' # state persists, no restart
Features
Feels like normal Julia. We mimic julia CLI: -e / -E, --project with the usual selectors (@., @temp, named shared envs), running a file directly (julia-client script.jl), and stdin piping. If you know julia, you already know this.
Just a CLI. No MCP, no client config, no JSON wiring. Works in a terminal, a bash script, or an agent. Project env auto-detected by walking $PWD for Project.toml.
Single binary, zero runtime deps. Pure Go. curl | bash and go.
Live streaming output. stdout and stderr come back as separate NDJSON frames as they happen, captured at the OS-pipe level — not buffered until the call ends.
Robust errors via a dedicated control channel. Errors are read off a separate fd-3 frame, never scraped from stdout. Tiered tracebacks: --trace short|smart|full (smart hides Julia/client internals, keeps your frames). Replay the last traceback without rerunning: julia-client trace.
Real interruption.julia-client interrupt sends SIGINT (then SIGKILL after 3s). Killing the client also interrupts the in-flight eval — so timeout 30 julia-client -e 'might_hang()' just works, no orphaned runaway computation.
Flexible session routing. Key by project path (default, --project=@.), by env selector (@temp, @shared), or by a named --session LABEL. --fresh resets a session.
Revise auto-reload for edited package code; --fresh for the cases Revise can’t track (struct redefs, new module deps).
Built for agents: the skill
An Agent skill ships in the repo, so assistants drive julia-client exactly the way you do — same flags, same sessions, no MCP server to stand up.
Could you clarify how sessions are launched and reused, and relative to the --project flag?
This reads like specifying different projects will launch new Julia sessions, which makes sense to avoid some unseen dependency incompatibilities. However, the README has this:
which seems to suggest different lines of the same “script” is being run in one session activating different projects, in sharp contrast to the example here:
A session’s project is fixed at launch. Passing a different --project does not re-activate the project of a running session — it routes to (and, if needed, starts) a different session. So different --project values are different processes with independent state, while the same key always reuses the same process. Use --session LABEL when you want one session reachable from multiple directories.
Nice to see this space explored! There are definitely pros/cons to both the julia-mcp and this kind of standalone CLI tool approach. The main tradeoff is:
MCP: (+) no lifecycle/lingering daemons to manage, julia sessions are scoped to AI sessions, naturally handles multiple independent sessions per julia env, works on all OS; (-) AI-specific, infeasible to use from outside.
standalone CLI: (+) benefits all of AI / users / scripts equally, can be used anywhere; (-) necessary to have long-running background processes, need special plumbing to get multiple independent julia sessions for the same folder.
I don’t know if it’s possible to somehow get best of both worlds…
// also cc @tecosaur who worked on something similar recently
DaemonMode does cover the core value prop — warm process, persistent snippet state via runexpr, live output streaming. Confirmed by running it.
Where it genuinely differs (all verified, not inferred):
- No stream separation — stdout and stderr are merged.
- No structured/leveled errors — raw stacktrace only, no smart filtering or trace replay.
- No session isolation — single shared Main; concurrent clients can clobber each other's globals.
- No interrupt and no disconnect-cleanup — a killed/timed-out client leaves its computation running server-side to completion.
- No auto result display, no per-session threads.
So the earlier "when to use which" holds, now grounded:
- DaemonMode — fine when you want one warm shared scope and either fast stateless script runs (runargs) or simple persistent snippets (runexpr), and you don't care about isolation, interruptibility, or machine-parseable output. Pure-Julia, cross-platform.
- julia-client — the things it adds over DaemonMode are exactly the things it tests and DaemonMode lacks: keyed/isolated sessions, separated stdout/stderr/error channels, leveled tracebacks, real interrupt + client-disconnect cleanup, and a dependency-free (Go) client. That's what makes it the better tooling/agent backend.
Personally I found it quite usefully when you can use julia-client --project=test ..., julia-client --project=benchmark ... and running experiment like julia-client --session=exp1 .... It tries to mimic julia command line, so AI is pretty efficient to combine with grep, timeout and all other command line utilities.
For multiple concurrent AI sessions I usually give each its own git worktree anyway, and since julia-client keys sessions by project path, separate worktrees get independent Julia sessions automatically, no extra plumbing.
What I really like about cli-based skill is that it is very tunable and cost 0 tokens until invoked. Skill lets each user/team/project re-tune the agent’s behavior to have different default routing, project-specific setup snippets by simply editing markdown.