Releases: dwgx/SmartCLI
Release list
v0.2.3 — control-plane concurrency
Install the three Claude Code skills — one zip, no git, no pip
curl -LO https://github.com/dwgx/SmartCLI/releases/latest/download/smartcli-skills.zip
unzip smartcli-skills.zip -d ~/.claude/skills/
309 KiB. cmd-art and tui-ui then run on CPython 3.10+ with no third-party
packages at all (verified on a bare virtualenv: 30 effects, 17 widgets).
drive-tui also needs pyte — pip install smartcli-toolkit.
Or the library / CLI / MCP server: pip install smartcli-toolkit
A control-plane concurrency release. The headline is a security fix that v0.2.2
did not carry: the daemon's serial accept loop let any local process — with no
credential at all — deny service for ~18s at a time, and pip install smartcli-toolkit
still had that. It also removes a second block that was one layer in: a long wait
kept a concurrent snapshot waiting behind it.
Fixed
- One connection could stall the daemon for every other caller. The accept loop
was serial with the UNAUTHENTICATED transport read inline, so any local process
could connect, send bytes with no newline, and head-of-line block every other
caller — measured at ~18s of denial from nine held connections, repeatable and
needing no credential. Now the accept thread only accepts, a per-connection reader
performs the unauthenticated work (read, parse, constant-time token check) so a
silent peer burns only its own 2s budget, and a single worker thread is the only
thread that touches the session. Measured after: an authenticated request is served
in 0.00s under the same attack. A previous release note described this as bounded
30x but not fixed; it is now fixed. - A long wait blocked unrelated fast verbs. A
wait-regex --timeout-ms 60000
occupied the session worker, so a concurrentsnapshotwaited behind it (8.00s,
measured).smartcli_core's four wait loops andPtySession's six wait methods
gained an optionalon_pollhook, invoked in the idle gap on the waiting
thread, so a fast verb is answered without a second thread ever entering the
session —PtySessionis not thread-safe (visual_hash()clearsscreen.dirty
as a side effect,pump()is read-modify-write,resize()mutates four fields in
sequence). DefaultNone, so every existing caller behaves identically. On a live
PTY:snapshotin 0.13s during a 20swait-regex. A second LONG wait still queues
behind the first — one session owns one PTY child, so that is inherent.
resizeis deliberately NOT answered mid-wait: it re-dimensions the pyte screen and
therefore changes the content hash, which would make a caller blocked in
wait-changeconclude its own keystroke had landed.
Added
tests/test_daemon_concurrency.py— drives the real accept loop against a fake
session (no PTY, no child process) and locks: an authenticated request is served
whilelistenbacklog + 1 silent peers hold connections; a 200 KB request spanning
manyrecv()calls still succeeds; auth is still enforced with no screen leak; a
fast verb is answered during a long wait; and session access provably stays on one
thread. Suite is 44 entries.tools/mcp_stdio_smoke.py, run bydocker.ymlagainst the built image — the check
an MCP directory performs (start the container with no arguments, speak JSON-RPC).
The image had shipped withCMD ["mcp"]for three releases with no test ever
running it.
Changed
test_perf_contractdeclines to measure timing ceilings when a tracer is attached
(coverage runs it under one), because the number measured there describes the
tracer. Raising the ceiling instead would have destroyed the 2000x regression window
the gate exists to protect.test_doc_countsgates README's quoteddrive_vimoutput against the example's
step()literals, and the four localized READMEs gained the 30-second quickstart
and thedrive_vimcomparison.
v0.2.2 — control-plane correctness
A control-plane correctness release. The headline is that close could delete a
live daemon's registry entry, stranding a real PTY child while the very command
this project tells you to use for confirming cleanliness reported zero sessions.
Everything else here either closes a security bypass or makes a check capable of
failing; there are no new features.
Fixed
closeafter a failed request deleted the registry entry of a LIVE daemon.
_callturns any transport failure — including a plain timeout — into a
SystemExittelling the operator to runclose --id <sid>"to clean up the stale
entry", andclosethen unlinked the file unconditionally. But the daemon's accept
loop is serial, so a busy daemon is indistinguishable from a dead one at the socket,
and that file is the only store of both the capability token and the pid. The
documented recovery could therefore leave a live daemon owning a running PTY child
that was unreachable by protocol (token gone) and unfindable for a manual kill (pid
gone) — whilecloseprintedclosed <sid>, exited 0, andlistreported zero
sessions. Since this project's own guidance is to confirm "zero leaked sessions" with
exactly thatlist, the check would have confirmed a lie. Death is now proven before
deletion (os.kill(pid, 0)on POSIX,OpenProcess+GetExitCodeProcesson
Windows, where signal 0 does not exist);closerefuses and exits 1 with the pid, and
the new--forceoverrides it while saying in its help what that costs.--envcould re-inject the session token on Windows. The control-plane guard was
key.startswith("SMARTCLI_TUI_")— an exact-case check — while Windows environment
names are case-insensitive and CPython upcases keys on assignment. So
--env smartcli_tui_token=…passed validation andos.environ.update()installed it
asSMARTCLI_TUI_TOKENin the driven child: precisely the capability the daemon
pops so a child cannot control its own session. Now compared uppercased
unconditionally, with the deny-list widened toSMARTCLI_ROOT,
SMARTCLI_MAX_SESSIONSandSMARTCLI_AUTO_INSTALL. Your own variable names are
unaffected.- An unauthenticated peer could head-of-line block the daemon for 60s per
connection.conn.settimeout(60.0)was set before the token check, so any local
process could connect, send bytes with no newline, and stall every other caller with
no credential at all; withlisten(8), nine such connections starve the owner. The
budget is now split — 2s pre-auth, re-armed against a fixed deadline so a peer
dribbling bytes cannot renew it, and 60s only for an authenticated caller's reply.
Measured: nine held connections went from 540s to 18s. That is a 30× reduction and
NOT a fix — the residual is inherent to the serial accept loop. Per-connection
threading is the real answer and is deliberately not attempted here, because
PtySessionis not thread-safe;SECURITY.mdnow documents the residual instead of
claiming the bound prevents it. - A non-dict JSON request was answered with an interpreter exception, pre-auth.
[1,2,3]reached the handler and died onreq.get();AttributeErrorwas not in the
connection guard's tuple, so an unauthenticated peer received
{"error": "AttributeError: …"}with nookfield, unlike every other reply on the
socket. Rejected explicitly now, before dispatch. examples/drive_vim.pysent five keystrokes blind and did not setTERM. The
mode changes (Escape,G,o) were issued back to back with nothing between them —
the blind send this project exists to argue against — so under load the keystrokes
were swallowed and nothing was inserted. It now confirms-- INSERT --before typing,
which proves bothGandolanded. And without aTERMvim never enters the
alternate screen nor saves the file, so two of the six steps failed for the absence of
an environment variable rather than for anything in the code; it is set explicitly now.
Changed
tests/run_all.pyno longer reports success for a gate that was deleted. 29 of 43
entries wereoptional=True, including 20 committed deterministic gates, so renaming
or removing any of them was a green SKIP — while the runner is documented as
pass-or-fail. A missing file that git tracks is now a FAIL regardless of the flag,
which is derived rather than hand-maintained and so covers a new gate the moment it is
committed. Entries that depend on an external binary (tmux, vim, less) still skip
themselves internally, so a green run on a host lacking those covers less.run_all.pyretains and prints child output on failure (last 40 lines), and
surfaces internalSKIP:lines even on a PASS. Previously a suite failure was
reportable only as an exit code and had to be re-run standalone — exactly the case
where an order- or load-dependent failure does not reproduce.- Anti-drift gates that could not fail, fixed.
test_fx_contract's exact-width
contract was gated on a predicate that evaluated the very condition it asserts, so any
effect violating it was reclassified "sparse" and passed; the classification is now
a frozen 24-name set with a second check so it cannot rot in either direction, and
skipped contracts are no longer counted as passes (the summary reads
174/174 passed, 6 skipped, where "150/150" had included six checks that never ran).
test_doc_countsexempted its own authoritative counts line by inferring intent from
nearby words — exemption is now an explicitdoc-counts:ignoremarker — and it now
gates the recipe count it had only been printing. The dependency gate's Homebrew
half ran a pip-shaped regex over a Ruby formula and could not match under any
circumstances; each draft is now parsed in its own syntax. tests/_tmux_launcher_probe.pyread the new pane the instant the launcher
returned. The single-effect branch ends inexec tmux split-window, which returns
when the pane exists — measured, the first frame arrives ~0.5s later — so it
sampled a legitimately blank screen. It now polls for the condition with a bound. It
also setsTERM, without which tmux refuses to attach a client and two more checks
failed for a rig reason.
Notes
- No API or behaviour change for library users beyond
close's new refusal (and the
--forceescape hatch).smartcli_core's public surface is unchanged. tests/run_all.pyis 43/43 on macOS with no FAIL, no SKIP and no rerun.
v0.2.1
A perception-correctness release. The headline is not a feature: an upgrade of
pyte alone could have blanked the primary screen for every 0.2.0 user, and
this release defuses that before it ships upstream. Everything else is the
alternate-screen work reaching the surfaces an agent actually reads, plus five
more measured emulation fixes.
Fixed
- Two dependency timebombs, defused by capability detection rather than a
version pin.pyte>=0.8.1is an open range in bothrequirements.txtand
pyproject.toml, so the day upstream ships its own alternate screen
(selectel/pyte#212, which this
project authored), subclass and base class would both switch — restoring a
BLANK primary screen on every full-screen program exit. Measured:['', '', ''].
The second isdelete_characterswidening DCH over a wide glyph; against a pyte
that does the same,中x+ CR + DCH went from"x"to"", silently eating a
character. Both now ask the installed pyte what it can do (_PYTE_HAS_ALTvia
hasattr,_PYTE_DCH_HANDLES_WIDEvia a one-shot behavioural probe). A cap
would have kept users off the upstream fix forever and needed revising every
release. Verified under BOTH stock 0.8.2 and a patched checkout, because a
one-sided test cannot distinguish "correct" from "the branch that happens to run
here". CUD(cursor down) was missing its DECSTBM override.index()and
cursor_up()were overridden for exactly this defect class; their mirror was
not, so from below a scroll regionESC[3;6r ESC[8;1H ESC[1Blanded on row 6
where tmux and GNU screen both give row 9. Found by asking why the third
override was absent — a gap a generative fuzzer cannot surface, because it
generates sequences, not absences.DL(delete lines) left the rows it vacated populated instead of blanking
them.- Resizing while on the alternate screen clipped the saved primary screen
correctly, restored the pen along with the cursor, and left the alternate screen
on RIS. - Mode 1048 no longer collides with 1049's save slot. Adding 1048 initially
routed it through the same_alt_savepointas 1049, reintroducing the defect
the dedicated slot was created for one commit earlier. - The MCP
snapshottool silently droppedalt_screen. The daemon has always
sent it and the CLI has always printed it, so MCP clients — the surface this
project promotes hardest — were the only ones that could not tell whether a
full-screen program owned the screen. That is precisely the blindness the
alternate-screen work exists to remove. - The mypy gate was checking a state that does not exist. CI installed only
ruffandmypy, sopytewas absent,ignore_missing_importsdegraded
pyte.ScreentoAny, and a correcttype: ignorewas reported as unused
while two genuine errors present since 0.2.0 went unseen. The gate now installs
the runtime dependencies and is mutation-verified to still bite. examples/drive_vim.pyruns from a source checkout, not only an install, and
the driven test fixtures no longerimport msvcrtunconditionally — that alone
was four of the suite's failures on POSIX.
Added
- Private mode 1048 (cursor save/restore without the buffer switch), with its
weaker evidence level stated in the code: xterm defines it, but neither
reference emulator implements it, so there is no ground truth to check against. alt_screenon every surface an agent reads —ScreenModel,Snapshot,
theto_text()header (it leads the flags, because it changes what an action
MEANS), the JSON hints, and every drive-tui daemon reply. Previously reachable
only by poking the underlying pyte object.tui.py resize— the daemon and MCP had supported resize since the control
plane was hardened; the CLI had no verb. A rejected size returns an error and
leaves the session alive.tests/test_terminal_fidelity.pylocks, including DECCOLM against the alternate
screen, and a cross-platformgetwch()(tests/_kbd.py) that enters raw mode
once rather than per keystroke — otherwise anESC [ Agets split across three
separate raw-mode entries.RESEARCH-PROMPTS.md— the calibrated research anchors, each recording what a
good answer would actually change in the backlog.- CLI coverage for
resizeintests/_tui_cli_probe.py, including the check that
a rejected size leaves the session alive._validate_sizeraisesSystemExit,
aBaseExceptionthat would otherwise pass straight through the daemon's
per-connectionexcept Exceptionand tear the session down; nothing had pinned
that from the CLI side.
Fixed after an adversarial self-review
The release was reviewed before tagging. Five real defects came back, all
introduced by this release's own work, and all fixed here — four found by
independent adversarial agents, and the last (the version contradictions) found by
reading back over the release commit rather than by any agent or gate:
- The lint-gate fix would have re-broken the gate from the other direction. The
type: ignore[misc]onsuper().alternate_screenis correct only while pyte
lacks the attribute; the day pyte ships it,warn_unused_ignoresfails on an
ignore that has become unused. Reproduced by injecting the attribute. Replaced
withgetattr(super(), "alternate_screen", False), which needs no ignore in
either state — a version-dependent ignore would have needed revising on the very
release that makes the capability check unnecessary. resizewas invisible intui.py --help: the subparser metavar is a
hand-maintained string and the new verb was never added to it.- A rejected resize printed
error: error: ...— the daemon stored
_validate_size's already-prefixed message while every other daemon reply
stores a bare one for_callto prefix once. - The HANDOFF continuation prompt listed six portable pyte defects including
IL/DL cursor column. It is five, and IL/DL is explicitly excluded — filing it
upstream would have been rejected, since pyte matches the standard there and
this project is the deviation. That prompt is what a fresh session pastes and
follows, so the error was one step from becoming a bad upstream patch. - Two version contradictions inside HANDOFF.md — a
VERSION = 0.2.0line nine
lines below the 0.2.1 banner, and a "read this first" pointer still routing to
work from two rounds earlier. The ten-site version gate cannot see prose.
Changed
tests/run_all.pyis 43/43 on macOS, the first full green on this host. The
four prior failures were platform gaps in test fixtures, not product bugs — and
with four known failures a genuine regression was indistinguishable from the
noise floor.- Documented as a deliberate CHOICE rather than a bug: IL/DL keep the cursor
column. An independent re-check found pyte matches xterm, vte and the DEC VT
reference here; tmux, GNU screen, urxvt, konsole and linuxvc keep the column as
this project does. Five implementations against two, so the behaviour stays —
but it must not be upstreamed, and the docstring that had asserted "real
terminals keep the column" now records the full split. - GitHub Actions pins brought current — 27 pins across 9 workflows
(checkoutv4→v7,setup-pythonv5→v7,deploy-pagesv4→v5,
configure-pagesv5→v6,upload-pages-artifactv3→v5,login-actionv3→v4,
codeql-actionv3→v4), clearing a seven-PR Dependabot backlog and the Node 20
deprecation warning on every run. The breaking changes were read rather than
assumed:checkoutv7 blocks fork-PR checkout underpull_request_target/
workflow_runandsetup-pythonv7 removed thepip-installinput — this repo
uses neither.mkdocs-materialdocs-build floor raised to 9.7.7.
Install: pip install smartcli-toolkit · Plugin: /plugin marketplace add dwgx/SmartCLI · MCP: io.github.dwgx/smartcli
v0.2.0 — hardened control plane, installable MCP, terminal fidelity
Security hardening of the drive-tui control plane, an installable MCP surface,
and — from a differential-testing campaign against real terminals — twelve
screen-emulation bugs fixed, including one that made every full-screen TUI
unreadable. See HANDOFF §10 for the full arc.
Added
- Installable
smartcli-tui,smartcli-mcp, and registry-compatible
smartcli-toolkitconsole commands; the wheel now includes the drive/MCP
implementation instead of shipping onlysmartcli_core. cwdand repeatedKEY=VALUEenvironment controls for persistent sessions,
machine-readable start/list/close output, and structured MCP snapshots.visual_hash+wait_visual_changeacross core, daemon, CLI, one-shot steps,
and MCP for attribute-only selection and cursor movement.- Alternate screen buffer support (modes 1049/1047/47) with
ScreenModel.screen.alt_screen. pyte implements none of these, so until now a
full-screen program (vim, less, htop) painted its alternate screen on top of
the main one and never restored it — an agent read a merged, impossible screen. - SGR sub-parameter tolerance (ITU-T T.416
:syntax, e.g.ESC[4:3m,
ESC[38:2::R:G:Bm), which pyte's parser aborted on, drawing the remainder of
the sequence onto the grid as literal text. Neovim, kitty and delta emit it. - Differential test suite against real terminals:
_diff_tmux_pyte.py(35
curated cases vs tmux),_diff_two_refs.py(tmux AND GNU screen; ground truth
only where both agree),_diff_fuzz_tmux.py(generative VT fuzz),
_tmux_launcher_probe.py, plus deterministic locks in
test_terminal_fidelity.py. test_perf_contract.py— the first performance test in the suite — and
test_readiness_properties.py(Hypothesis invariants for the wait primitives).test_version_sync.py, a ten-site version anti-drift gate; widget-count and
dev-box-path gates intest_doc_counts.py.- Cross-platform package/MCP smoke jobs and Python 3.10/3.14 CI boundaries.
- OIDC MCP Registry publishing after a successful PyPI tag release.
Fixed
- Explicit
wait_changebaseline hashes are integers end to end; CLI/MCP calls
no longer report an immediate false change because of a string/int mismatch. - Session ids can no longer traverse outside the registry directory, registry
writes refuse symlinks on POSIX, and controlled children no longer inherit
the daemon capability token. - Detached session count is bounded (8 by default, configurable up to 128),
stale close actually removes its registry entry, and MCP close is idempotent. - An out-of-range
resizeno longer kills the daemon and its live session
(SystemExitescaped the per-connection guard). - Screen-emulation fidelity, each divergence measured against real tmux and,
where it could arbitrate, GNU screen: IL/DL no longer home the cursor column;
IL with count > 1 no longer leaves buffer holes that make a later DL delete the
wrong row; half-overwriting a wide glyph blanks it instead of dropping the
incoming character; DCH removes both cells of a wide glyph; NEL returns to
column 0; a cursor outside a DECSTBM region is neither dragged into it nor
clamped by it; a two-column glyph with one column left wraps whole; an
overwritten wide base leaves no orphaned stub; and a zero-width joiner or
variation selector no longer truncates the rest of the write ("MENU ♀️ Settings Quit"used to be perceived as"MENU ♀"). visual_hashis incremental — 16.6 ms → 0.008 ms per idle poll on a 300x100
screen, where it previously consumed 55% of the 30 ms polling budget.fx-popup.shrefuses cleanly when no tmux client is attached instead of
leaking tmux'sno current clientwith a non-zero exit.- Real-session probes use the running Python interpreter with platform-correct
quoting instead of assuming apythoncommand exists on PATH (not true on
current macOS installations).
Changed
- Python 3.10 is now the supported floor because the packaged MCP surface uses
modern type syntax; the MCP SDK is a required package dependency souvx
launch from the official MCP Registry works without extra flags.
SmartCLI v0.1.2
Correctness release — deep review + mutation-testing pass, every fix with a repro and a regression-lock test.
Install
pip install -U smartcli-toolkitFixed
- readiness (#1) —
wait_ready/wait_until_stableno longer declare STABLE on a never-painted blank screen during a startup quiet-gap (optionalblank_hashgate; default off = old behavior). - docs (#2) — quickstart marker
>>> $never matched pyte's space-padded lines; examples now use unanchored>>>. - PTY backend (#4) —
WinptyBackend.spawnresets queue/EOF/reader so a re-used backend can't inherit a stale EOF sentinel. - degenerate-input crashes in skill code:
Ripple(wavelength/falloff 0, empty palette),SliderTrack(empty positions),BrailleChart(non-finite values), andfx Paramint coercion (08/010and±-signed based literals).
Added
- Regression-lock tests (
test_readiness,test_degenerate_inputs,test_fx_contract18×6,box_junctionself-test) + unifiedtests/run_all.py.
Full notes: CHANGELOG.md
SmartCLI v0.1.0
First public release of SmartCLI — three Agent Skills over one pluggable PTY + pyte core.
Install
pip install smartcli-toolkit(import name stays smartcli_core — e.g. from smartcli_core import PtySession)
Or reproduce the full dev environment:
git clone https://github.com/dwgx/SmartCLI && cd SmartCLI
pip install -r requirements.txtWhat's inside
- cmd-art — 18 terminal visual effects, 8 themes (
python -m fx) - drive-tui — 8 TUI-driving recipes + a persistent PTY daemon (perceive → decide → act → wait → confirm)
- tui-ui — 15 cell-accurate widgets + a rendering engine (field shaders, sub-cell raster, box-junction algebra, honest color degrade)
- smartcli_core — the shared pluggable PTY backend (ConPTY on Windows, POSIX pty elsewhere) +
pytescreen model + semantic snapshot + readiness sync - A 122-note knowledge graph, screenshot/AGENTCLI verification harnesses, MIT license, and CI.
Links
- PyPI: https://pypi.org/project/smartcli-toolkit/0.1.0/
- Full usage: README-USAGE.md
Cross-platform (Windows / macOS / Linux), pure standard library at the core.