[ANN] julia-client — persistent Julia sessions as CLI

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.

npx skills add https://github.com/Beforerr/julia-client

Links

Feedback welcome.

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:

# Explicit project environment
julia-client --project /path/to/project -e 'using MyPackage'
julia-client --project @temp -e 'using Pkg; Pkg.add("Example")'
julia-client --project @shareAnyname -e 'using Example'

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.

This has been around for many years and predates the LLM era:

When (in which scenarios) to use DaemonMode and when to use julia-client ?

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… :slight_smile:

// also cc @tecosaur who worked on something similar recently

Here is a short summary from Claude

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.

Thanks and very much inspired by you.

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.