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.

Packages

Package details

extension

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 cd chains 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 with terminal_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 an outputSchema), so pi's codemode scripts and nested ctx.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 — /term is 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 bash transparently 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 via terminal_tools, the built-in tool_search, or a defaultTools pin.
  • 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 uses pi.registerToolRenderer(), so calls to deferred/unregistered tools — including terminal_* 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-pty fails 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 with searchTools() and read usage guidance via describeNamespace("smart-terminal").
  • Structured results — every tool declares an outputSchema and returns structuredContent; the model still receives the JSON text you know.
  • Failure with data — terminal_run (failed success criteria), terminal_wait (pattern never appeared) and terminal_watch (timeout without a match) return isError: true results: the model sees the failure, scripts still get the structured payload. terminal_exec keeps exit codes as data (MCP semantics).
  • Annotations — observers (terminal_read/list/wait/watch/get_history/run_paged) carry readOnlyHint, terminal_stop / terminal_write_file carry destructiveHint, 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-in tool_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 every terminal_* call as a one-line summary plus a status badge: exit code and drifted cwd for terminal_exec/terminal_retry, match result for terminal_wait/terminal_watch, session count for terminal_list, colored +/− stats for terminal_diff. Because the resolver works by tool name rather than registration, it also renders calls to deferred tools that were never activated, and terminal_* 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: cd carries over between calls. The per-call cwd is only used when (re-)creating a dead session. Watch drift in the footer, or reset with terminal_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+c to the PTY) map onto pi's native error formats; partial output is preserved.
  • Long-running commands (dev servers): use terminal_exec with quietExitMs, or let a bash call time out — the command keeps running in the session; read new output with terminal_read({ since }), wait for patterns with terminal_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):

  1. PI_TERMINAL_SHELL env var: powershell, cmd, bash, any name on PATH, or an absolute path.
  2. defaultShell in ~/.pi/agent/smart-terminal.json.
  3. Auto-detect: pwsh > powershell > cmd on Windows, $SHELL > bash > sh elsewhere.

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_start without a shell argument, 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.
  • -NoProfile survives: 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) and PI_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_start and terminal_list return shell and shellType per session, and the per-request terminal_sessions context 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 markers terminal_exec appends 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:

  1. Write the payload to a file: the write tool, or terminal_write_file (resolves relative paths against the session's drifted cwd).
  2. 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, f follow, q close)
  • /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