shell/ compatibility extension — a DROPPABLE classpath plug-in (drop the
jar, drop the feature), gated behind the user-owned :shell/enabled toggle
(OFF by default; every call short-circuits into a refusal envelope until the
user flips it in settings).
Three model-facing bindings under alias shell (the flat Python sandbox
renders alias/name as <alias>_<name>, so the calls are
shell_run / shell_bg / shell_logs — same shape as git_status,
clj_eval, search_web):
SYNC shell_run(cmd) / shell_run(cmd, opts) — bash -lc in the
workspace root, waits up to a timeout, and returns a LEAN payload with
string keys cmd/stdout/duration_ms + conditional keys (exit when finished;
timed_out/timeout_secs on timeout; stderr when non-empty; truncation
flags when true; cwd when narrowed) — results ride every later prompt
as frozen <results> pins, so dead fields never ship. Output is bounded
at read time to a head+tail budget per stream — only the MIDDLE of a huge
stream is dropped, never its start or end (memory can't balloon on a
chatty-then-killed command). A non-zero exit is DATA the
model reads, not a tool error.
BACKGROUND shell_bg(id, cmd) — spawns the process, pumps its merged
output into a bounded ring buffer, and registers it as a session
RESOURCE in internal.resources: it shows up in the footer count, the
F4 dialog, and the resources ctx block, and the ONE stop path
is resource_stop(id) (model) / the footer dialog (user) — both land
on resources/stop!, which runs our :stop-fn (process-tree kill +
buffer drop). An exited process is NOT auto-pruned (its :alive-fn
reports true while the buffer entry exists) so shell_logs(id) can
still read its output + exit code until the resource is stopped.
shell_logs(id) / shell_logs(id, n) — tail of a background shell's
captured output as [seq, line] tuples plus status/exit/uptime.
The :shell/enabled toggle is registered HERE, extension-owned under the
extension's own namespace.
`shell/` compatibility extension — a DROPPABLE classpath plug-in (drop the jar, drop the feature), gated behind the user-owned `:shell/enabled` toggle (OFF by default; every call short-circuits into a refusal envelope until the user flips it in settings). Three model-facing bindings under alias `shell` (the flat Python sandbox renders `alias/name` as `<alias>_<name>`, so the calls are `shell_run` / `shell_bg` / `shell_logs` — same shape as `git_status`, `clj_eval`, `search_web`): 1. SYNC `shell_run(cmd)` / `shell_run(cmd, opts)` — `bash -lc` in the workspace root, waits up to a timeout, and returns a LEAN payload with string keys cmd/stdout/duration_ms + conditional keys (exit when finished; timed_out/timeout_secs on timeout; stderr when non-empty; truncation flags when true; cwd when narrowed) — results ride every later prompt as frozen <results> pins, so dead fields never ship. Output is bounded at read time to a head+tail budget per stream — only the MIDDLE of a huge stream is dropped, never its start or end (memory can't balloon on a chatty-then-killed command). A non-zero exit is DATA the model reads, not a tool error. 2. BACKGROUND `shell_bg(id, cmd)` — spawns the process, pumps its merged output into a bounded ring buffer, and registers it as a session RESOURCE in `internal.resources`: it shows up in the footer count, the F4 dialog, and the `resources` ctx block, and the ONE stop path is `resource_stop(id)` (model) / the footer dialog (user) — both land on `resources/stop!`, which runs our `:stop-fn` (process-tree kill + buffer drop). An exited process is NOT auto-pruned (its `:alive-fn` reports true while the buffer entry exists) so `shell_logs(id)` can still read its output + exit code until the resource is stopped. 3. `shell_logs(id)` / `shell_logs(id, n)` — tail of a background shell's captured output as `[seq, line]` tuples plus status/exit/uptime. The `:shell/enabled` toggle is registered HERE, extension-owned under the extension's own namespace.
(shell-attach-command _parsed residual)vis ext shell attach <id> — the human-side passthrough: join a live
shell_bg shell's PTY in your OWN terminal (finish a browser OAuth, answer a
prompt only a person can), then Ctrl-] to detach with the child untouched.
--socket PATH targets an explicit socket; otherwise the newest shell whose
id matches. Returns the attach exit code.
`vis ext shell attach <id>` — the human-side passthrough: join a live `shell_bg` shell's PTY in your OWN terminal (finish a browser OAuth, answer a prompt only a person can), then Ctrl-] to detach with the child untouched. `--socket PATH` targets an explicit socket; otherwise the newest shell whose id matches. Returns the attach exit code.
(shell-bg id cmd)await shell_bg("dev-server", "npm run dev")
Start a background command (bash -lc, workspace root) as a session resource id; no timeout — use for daemons / watchers / long builds.
Returns {"id", "pid", "cmd", "status": "running"}.
Read output with shell_logs(id); stop and discard logs with resource_stop(id) — the ONLY stop path.
Gotcha: id must be unique among RUNNING shells; reusing an exited id discards its retained logs.
await shell_bg("dev-server", "npm run dev")
Start a background command (bash -lc, workspace root) as a session resource `id`; no timeout — use for daemons / watchers / long builds.
Returns {"id", "pid", "cmd", "status": "running"}.
Read output with shell_logs(id); stop and discard logs with resource_stop(id) — the ONLY stop path.
Gotcha: `id` must be unique among RUNNING shells; reusing an exited id discards its retained logs.CLI surface mounted under vis ext shell. Only attach for now — the human
passthrough onto a background PTY the agent spawned.
CLI surface mounted under `vis ext shell`. Only `attach` for now — the human passthrough onto a background PTY the agent spawned.
(shell-logs id)(shell-logs id n)await shell_logs("dev-server") await shell_logs("dev-server", 500)
Tail a background shell's captured output. shell_logs(id) keeps the last 200 lines, shell_logs(id, n) the last n (max 2000). Returns {"id", "status": "running"|"exited", "lines": [[seq, text], ...], "line_count", "uptime_ms"} plus, only when meaningful (use .get): "exit", "dropped". Gotcha: "lines" is [seq, text] pairs (not strings); shown count is len(lines), "line_count" is total-ever.
await shell_logs("dev-server")
await shell_logs("dev-server", 500)
Tail a background shell's captured output. shell_logs(id) keeps the last 200 lines, shell_logs(id, n) the last n (max 2000).
Returns {"id", "status": "running"|"exited", "lines": [[seq, text], ...], "line_count", "uptime_ms"} plus, only when meaningful (use .get): "exit", "dropped".
Gotcha: "lines" is [seq, text] pairs (not strings); shown count is len(lines), "line_count" is total-ever.(shell-prompt _env)Prompt fragment advertising the shell surface — ONLY while the toggle is ON (a blank string is filtered out of the extensions prompt block), so a disabled layer costs zero prompt tokens and the model never sees it.
Prompt fragment advertising the shell surface — ONLY while the toggle is ON (a blank string is filtered out of the extensions prompt block), so a disabled layer costs zero prompt tokens and the model never sees it.
(shell-run cmd)(shell-run cmd opts)await shell_run("git status") await shell_run("npm run build", {"timeout_secs": 300, "cwd": "web"})
Run a command via bash -lc in the workspace root. Returns {"cmd", "stdout", "duration_ms"} plus, only when meaningful (use .get): "exit", "timed_out"+"timeout_secs", "stderr", "stdout_truncated"/"stderr_truncated", "cwd". opts: {"timeout_secs": N (default 120, max 600), "cwd": rel-dir-inside-workspace}. Gotcha: a non-zero "exit" is DATA to read, not a tool failure. On timeout there is NO "exit" key.
await shell_run("git status")
await shell_run("npm run build", {"timeout_secs": 300, "cwd": "web"})
Run a command via bash -lc in the workspace root.
Returns {"cmd", "stdout", "duration_ms"} plus, only when meaningful (use .get): "exit", "timed_out"+"timeout_secs", "stderr", "stdout_truncated"/"stderr_truncated", "cwd".
opts: {"timeout_secs": N (default 120, max 600), "cwd": rel-dir-inside-workspace}.
Gotcha: a non-zero "exit" is DATA to read, not a tool failure. On timeout there is NO "exit" key.(shell-send id text)(shell-send id text opts)await shell_send("dev-server", "y") await shell_send("slack-auth", "xoxp-my-token", {"enter": true})
Type into a background shell's STDIN — the send-keys equivalent. The process runs under a REAL pty (isatty() true, $TERM set), so interactive prompts (password / read / REPL / y-N confirm) accept input. text is written to its stdin; with enter (default true) a trailing newline SUBMITS the line. Read what came back with shell_logs(id).
Returns {"id", "sent", "status"}.
Gotcha: only a RUNNING background shell accepts input; an exited one raises. A step only a HUMAN can finish (browser OAuth, a device-code prompt) can't be typed by the agent — tell the user to run vis ext shell attach <id> in their own terminal to take over the live session, then detach with Ctrl-] (the child keeps running). The shell_bg result carries the exact attach command.
await shell_send("dev-server", "y")
await shell_send("slack-auth", "xoxp-my-token", {"enter": true})
Type into a background shell's STDIN — the send-keys equivalent. The process runs under a REAL pty (isatty() true, $TERM set), so interactive prompts (password / `read` / REPL / y-N confirm) accept input. `text` is written to its stdin; with enter (default true) a trailing newline SUBMITS the line. Read what came back with shell_logs(id).
Returns {"id", "sent", "status"}.
Gotcha: only a RUNNING background shell accepts input; an exited one raises. A step only a HUMAN can finish (browser OAuth, a device-code prompt) can't be typed by the agent — tell the user to run `vis ext shell attach <id>` in their own terminal to take over the live session, then detach with Ctrl-] (the child keeps running). The shell_bg result carries the exact `attach` command.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 |