pi-smart-terminal
Persistent PTY terminal sessions for pi: overrides the built-in bash tool with a stateful shell, adds terminal_* tools, a live session overlay (/term) and footer status. Powered by smart-terminal-mcp.
Package details
Install pi-smart-terminal from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-smart-terminal- Package
pi-smart-terminal- Version
0.6.0- Published
- Oct 26, 2026
- Downloads
- 493/mo · 247/wk
- Author
- ngsoftware
- License
- MIT
- Types
- extension
- Size
- 130.6 KB
- Dependencies
- 1 dependency · 2 peers
Pi manifest JSON
{
"extensions": [
"./src/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-smart-terminal
A real, persistent shell for pi — the agent's bash tool runs in
a live PTY terminal session instead of a throwaway shell, powered by
smart-terminal-mcp's core.
Why
pi's built-in bash tool spawns a fresh shell for every command. Each call
runs in a vacuum:
bash: cd packages/api && npm test # 'cd' is lost instantly — so the agent
bash: cd packages/api && npm run build # must chain && cd ../.. into everything
bash: export API_KEY=... # environment gone by the next call
bash: npm run dev # background server: output unreachable,
# process dead or orphaned after the call
bash: python # REPLs, npm-init prompts, ssh, docker
# login — anything interactive just hangs
With pi-smart-terminal, bash runs in one persistent PTY session — same
shell, same terminal, alive for the whole conversation:
bash: cd packages/api # the session is now IN packages/api
bash: npm test # just works — no chaining
bash: export API_KEY=... # still set on the next call
bash: npm run dev # keeps running; new output readable at any time
# (terminal_read / terminal_wait / terminal_watch)
/term # YOU watch the agent's shell live, as it types
In short:
- State that sticks — cwd, environment variables and background processes
survive between calls. The agent stops wasting tokens on
cdchains and absolute paths. - Interactive programs work — REPLs, installers with prompts,
ssh, test-watchers: a real TTY means they behave like they do in your own terminal, and the agent can answer prompts withterminal_write/terminal_send_key. - Long-running processes become usable — start a dev server once, then read its output incrementally, wait for a "listening on" line, or watch for errors — event-driven, instead of re-dumping logs into the conversation.
- Codemode can drive it — every tool returns typed
structuredContent(with anoutputSchema), so pi'scodemodescripts and nestedctx.executeTool()callers orchestrate sessions programmatically (run → parse → retry loops) without parsing JSON text. - Session state survives compaction — the live session list (ids, cwd, busy) is injected into the model context on every request, so the model never forgets its sessionIds or their drifted cwd — even after compaction.
- You can actually watch it —
/termis a live viewer of the agent's shell. See what the model is doing in real time, scroll its full history, while the footer shows busy state and cwd at a glance (auto mode stays silent while idle). - Nothing leaks — all sessions are killed (entire process group) when pi exits; no orphaned dev servers after a session.
The terminal_* tool family adds the controls a persistent shell needs
(terminal_read, terminal_wait, terminal_watch, terminal_retry,
terminal_diff, …) — payload-identical to the smart-terminal-mcp tools, so
agent habits transfer 1:1 from Claude Code, Cursor & co.
How it integrates
- bash override — built-in
bashtransparently executes in the persistent PTY session (pi's rendering, truncation and timeouts preserved). Falls back to a one-shot shell when the session is busy with a background command. - terminal tools — 15
terminal_*tools matching the MCP server; extras load on demand viaterminal_tools, the built-intool_search, or adefaultToolspin. - compact tool rendering — every
terminal_*call renders as a one-line summary with a status badge (exit code, matched pattern, session count…); on pi ≥ 1.0.1 this usespi.registerToolRenderer(), so calls to deferred/unregistered tools — includingterminal_*calls in resumed sessions and HTML exports — render compactly too. /term— live session viewer (Tab / Shift+Tab switches sessions, structured header, three-zone status bar); footer — auto shows busy/exited states only.- Shared shell — opt-in: your
!commands run in the agent's session too. - Lifecycle — all PTYs killed (process group) on shutdown; if
node-ptyfails to load, pi starts normally without this extension.
Codemode, tool_search and error semantics
The tools are first-class citizens of pi's codemode ecosystem (pi ≥ 0.99):
- Namespace
smart-terminal— codemode scripts find the tools withsearchTools()and read usage guidance viadescribeNamespace("smart-terminal"). - Structured results — every tool declares an
outputSchemaand returnsstructuredContent; the model still receives the JSON text you know. - Failure with data —
terminal_run(failed success criteria),terminal_wait(pattern never appeared) andterminal_watch(timeout without a match) returnisError: trueresults: the model sees the failure, scripts still get the structured payload.terminal_execkeeps exit codes as data (MCP semantics). - Annotations — observers (
terminal_read/list/wait/watch/get_history/run_paged) carryreadOnlyHint,terminal_stop/terminal_write_filecarrydestructiveHint, so permission extensions can gate them accordingly. - Deferred extras — the 8 extra tools register with
exposure: "deferred": callable by codemode scripts and findable by the built-intool_searchat any time, but never auto-declared to the model. Activate them with theterminal_toolsloader, or pin e.g."defaultTools": ["+terminal_watch"](a pin survives; pi ≥ 0.99.2 re-enables it via/reload` too). - Compact rendering (pi ≥ 1.0.1) —
pi.registerToolRenderer()draws everyterminal_*call as a one-line summary plus a status badge: exit code and drifted cwd forterminal_exec/terminal_retry, match result forterminal_wait/terminal_watch, session count forterminal_list, colored+/−stats forterminal_diff. Because the resolver works by tool name rather than registration, it also renders calls to deferred tools that were never activated, andterminal_*calls in resumed sessions and HTML exports — even when the tool behind them is not registered (rendering is display-only and independent of the native PTY chain). On older pi the registration is skipped and pi's default rendering applies.
The session-state injection uses pi's context_with_system boundary: one
trailing system message per request carrying a terminal_sessions prompt
section (the same mechanism as pi's mcp_servers section). It is stateless —
never persisted to history — so the transcript stays clean and the provider
prefix remains cache-stable.
Install
pi install npm:pi-smart-terminal
Requirements: pi ≥ 0.99.2 (compact terminal_* rendering via registerToolRenderer on pi ≥ 1.0.1), Node ≥ 20 and a C++ toolchain for node-pty (prebuilt
binaries cover common platforms; on Windows use npm rebuild node-pty inside
the package if needed).
Configuration
~/.pi/agent/smart-terminal.json (all keys optional):
{
"overrideBash": true,
"userBash": false,
"bashTimeoutMs": 600000,
"footer": "auto",
"defaultShell": null,
"allToolsActive": false,
"sessionStateContext": true
}
| Key | Default | Meaning |
|---|---|---|
overrideBash |
true |
Replace built-in bash with the persistent-session backend. |
userBash |
false |
Route user ! commands through the shared session (opt-in; full-screen tools like !vim are better left native). |
bashTimeoutMs |
600000 |
Hard cap when the model passes no timeout. |
footer |
"auto" |
Footer status line: "auto" (only while a command runs or a session exited), "minimal" (always-on glyph + count, e.g. ⏵ 2), "full" (verbose id/name/cwd line), "off". true/false still work (= full/off). |
defaultShell |
null |
Force the shell for every session in this process; null = auto-detect (pwsh > powershell > cmd on Windows, $SHELL > bash > sh elsewhere). The PI_TERMINAL_SHELL env var wins over this key. |
allToolsActive |
false |
Register all 15 tools with full schemas instead of lazy loading extras. |
sessionStateContext |
true |
Inject live session state (ids, cwd, busy) into the model context each request — survives compaction. |
How the bash override behaves
- First call spawns the agent session and waits for the shell banner (~1–2.5 s once).
- cwd persistence is the point:
cdcarries over between calls. The per-call cwd is only used when (re-)creating a dead session. Watch drift in the footer, or reset withterminal_stop+ a fresh bash call. - Parallel bash calls / busy session (a background command is running): transparently falls back to a one-shot stateless execution so the call still succeeds. The fallback runs in the same pinned shell and announces itself with a trailing notice line, so nothing ever switches dialect silently (see Which shell do I get?).
- Timeout (
timeout:N) and abort (Esc →ctrl+cto the PTY) map onto pi's native error formats; partial output is preserved. - Long-running commands (dev servers): use
terminal_execwithquietExitMs, or let a bash call time out — the command keeps running in the session; read new output withterminal_read({ since }), wait for patterns withterminal_wait/terminal_watch, and watch it live in/term.
Which shell do I get?
Exactly one shell per pi process: chosen on first use, then pinned for the whole session. Determinism is the point. A command string that worked a minute ago must not start failing because the shell silently changed underneath it.
Resolution order (first match wins):
PI_TERMINAL_SHELLenv var:powershell,cmd,bash, any name on PATH, or an absolute path.defaultShellin~/.pi/agent/smart-terminal.json.- Auto-detect:
pwsh > powershell > cmdon Windows,$SHELL > bash > shelsewhere.
Guarantees:
- Pinned once per process: the first resolution is memoized, so a PATH change, a session restart or a cwd switch can never flip the shell mid-conversation.
- Invalid pin fails loudly: a typo reports the bad value plus what auto-detect would pick, instead of quietly reverting. Quiet reverting would recreate the exact surprise the pin exists to remove.
- Every path shares the pin: the agent session,
terminal_startwithout ashellargument, and the one-shot fallback for busy sessions all run the same shell. Before this, the fallback used the platform shell (cmd.exe on Windows), which is how commands flipped between PowerShell and cmd.exe mid-session. -NoProfilesurvives: a pinned PowerShell keeps-NoLogo -NoProfile, so a user profile cannot change aliases or prompts behind the agent's back. This matters because profiles vary per machine and would reintroduce the nondeterminism the pin is meant to remove.
Knowing the shell from the outside
- Prompt guideline: the bash tool's prompt declares the dialect plus a
concrete example (
The bash tool runs in one persistent PowerShell session (...)), so the agent writes correct syntax before its first command. - Env vars:
PI_SHELL(executable) andPI_SHELL_TYPE(powershell|cmd|bash) are exported when the shell resolves. Child processes (the fallback,terminal_run, your own scripts) can branch on them instead of guessing. - Tool payloads:
terminal_startandterminal_listreturnshellandshellTypeper session, and the per-requestterminal_sessionscontext section lists each live session's dialect.
Degradation is announced, not silent
When the shared session is busy (parallel bash call, background command), the
call still runs as a stateless one-shot in the same shell, and the result ends
with a notice line naming that shell plus a reminder that cd and env from the
shared session do not apply. This turns a confusing "the shell changed" report
into a diagnosable event.
One deliberate exception: terminal_run with shell: true is documented as a
one-shot against the system shell (cmd.exe on Windows). Pass shell: false
and an explicit executable when you need the pinned shell's semantics.
Multi-line payloads (here-strings, heredocs)
The PTY feeds the shell line by line, so multi-line shell constructs pasted in one go get mangled:
- PowerShell here-strings (
@'...'@): PSReadLine can eat the@'opener mid-paste, and every apostrophe in the body then breaks parsing (Unexpected token ...). - POSIX heredocs (
<<EOF): the completion markersterminal_execappends land on the closing-delimiter line, so the heredoc never terminates and the session hangs until the timeout. - cmd.exe multi-line: rejected up front with the same workaround. PowerShell
gets a byte-exact base64 wrapper for multi-line payloads; cmd.exe has no
equivalent, so a script typed line by line can wedge the prompt at
More?.
That's why terminal_exec (and the bash override, which uses the same
path) rejects them up front with the workaround instead of hanging the
session. Do this instead:
- Write the payload to a file: the
writetool, orterminal_write_file(resolves relative paths against the session's drifted cwd). - Reference the file:
gh pr create --body-file .pr-body.md,command < input.txt, or--body "$(Get-Content file -Raw)".
terminal_run is unaffected: it spawns a one-shot process with no PTY and no
markers, so heredocs genuinely work there. And if a command ever times out
with the shell stuck at a continuation prompt (>> / > / More?), the
result's hint names the prompt and how to recover: send the closing line
with terminal_write, or abort with terminal_send_key("ctrl+c").
Commands
/term— live overlay of terminal sessions (opens the most recent; Tab / Shift+Tab to switch, ↑↓/PgUp/PgDn scroll,ffollow,qclose)/terminals— list active sessions
Tool catalog
Active from the start: terminal_start, terminal_exec, terminal_run,
terminal_read, terminal_write, terminal_wait, terminal_stop,
terminal_list, plus the terminal_tools loader.
Deferred (load on demand): terminal_run_paged, terminal_retry,
terminal_diff, terminal_resize, terminal_send_key, terminal_get_history,
terminal_write_file, terminal_watch. Codemode scripts can call them as-is;
tool_search finds them; terminal_tools or a defaultTools pin activates
them for the model.
Payload shapes are identical to the smart-terminal-mcp tools, so agent habits transfer 1:1 between pi and MCP clients.
Relationship to smart-terminal-mcp
This package is the native pi integration of the same core (smart-terminal-mcp): one core, two adapters. The MCP server remains the portable path for Claude Code, Cursor and friends; inside pi, this extension goes deeper than MCP can — tool schemas, dynamic loading, custom UI, lifecycle hooks.
License
MIT