Skip to content

Releases: dwgx/SmartCLI

v0.2.3 — control-plane concurrency

Choose a tag to compare

@dwgx dwgx released this 09 Aug 03:45

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 pytepip 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 concurrent snapshot waited behind it (8.00s,
    measured). smartcli_core's four wait loops and PtySession's six wait methods
    gained an optional on_poll hook, invoked in the idle gap on the waiting
    thread
    , so a fast verb is answered without a second thread ever entering the
    session — PtySession is not thread-safe (visual_hash() clears screen.dirty
    as a side effect, pump() is read-modify-write, resize() mutates four fields in
    sequence). Default None, so every existing caller behaves identically. On a live
    PTY: snapshot in 0.13s during a 20s wait-regex. A second LONG wait still queues
    behind the first — one session owns one PTY child, so that is inherent.
    resize is 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-change conclude 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
    while listen backlog + 1 silent peers hold connections; a 200 KB request spanning
    many recv() 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 by docker.yml against the built image — the check
    an MCP directory performs (start the container with no arguments, speak JSON-RPC).
    The image had shipped with CMD ["mcp"] for three releases with no test ever
    running it.

Changed

  • test_perf_contract declines 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_counts gates README's quoted drive_vim output against the example's
    step() literals, and the four localized READMEs gained the 30-second quickstart
    and the drive_vim comparison.

v0.2.2 — control-plane correctness

Choose a tag to compare

@dwgx dwgx released this 09 Aug 00:43

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

  • close after a failed request deleted the registry entry of a LIVE daemon.
    _call turns any transport failure — including a plain timeout — into a
    SystemExit telling the operator to run close --id <sid> "to clean up the stale
    entry", and close then 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) — while close printed closed <sid>, exited 0, and list reported zero
    sessions. Since this project's own guidance is to confirm "zero leaked sessions" with
    exactly that list, the check would have confirmed a lie. Death is now proven before
    deletion (os.kill(pid, 0) on POSIX, OpenProcess + GetExitCodeProcess on
    Windows, where signal 0 does not exist); close refuses and exits 1 with the pid, and
    the new --force overrides it while saying in its help what that costs.
  • --env could 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 and os.environ.update() installed it
    as SMARTCLI_TUI_TOKEN in 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 to SMARTCLI_ROOT,
    SMARTCLI_MAX_SESSIONS and SMARTCLI_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; with listen(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
    PtySession is not thread-safe; SECURITY.md now 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 on req.get(); AttributeError was not in the
    connection guard's tuple, so an unauthenticated peer received
    {"error": "AttributeError: …"} with no ok field, unlike every other reply on the
    socket. Rejected explicitly now, before dispatch.
  • examples/drive_vim.py sent five keystrokes blind and did not set TERM. 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 both G and o landed. And without a TERM vim 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.py no longer reports success for a gate that was deleted. 29 of 43
    entries were optional=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.py retains and prints child output on failure (last 40 lines), and
    surfaces internal SKIP: 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_counts exempted its own authoritative counts line by inferring intent from
    nearby words — exemption is now an explicit doc-counts:ignore marker — 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.py read the new pane the instant the launcher
    returned.
    The single-effect branch ends in exec 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 sets TERM, 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
    --force escape hatch). smartcli_core's public surface is unchanged.
  • tests/run_all.py is 43/43 on macOS with no FAIL, no SKIP and no rerun.

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 06 Aug 13:00

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.1 is an open range in both requirements.txt and
    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 is delete_characters widening 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_ALT via
    hasattr, _PYTE_DCH_HANDLES_WIDE via 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 region ESC[3;6r ESC[8;1H ESC[1B landed 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_savepoint as 1049, reintroducing the defect
    the dedicated slot was created for one commit earlier.
  • The MCP snapshot tool silently dropped alt_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
    ruff and mypy, so pyte was absent, ignore_missing_imports degraded
    pyte.Screen to Any, and a correct type: ignore was 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.py runs from a source checkout, not only an install, and
    the driven test fixtures no longer import msvcrt unconditionally — 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_screen on every surface an agent readsScreenModel, Snapshot,
    the to_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.py locks, including DECCOLM against the alternate
    screen, and a cross-platform getwch() (tests/_kbd.py) that enters raw mode
    once rather than per keystroke — otherwise an ESC [ A gets 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 resize in tests/_tui_cli_probe.py, including the check that
    a rejected size leaves the session alive. _validate_size raises SystemExit,
    a BaseException that would otherwise pass straight through the daemon's
    per-connection except Exception and 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] on super().alternate_screen is correct only while pyte
    lacks the attribute; the day pyte ships it, warn_unused_ignores fails on an
    ignore that has become unused. Reproduced by injecting the attribute. Replaced
    with getattr(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.
  • resize was invisible in tui.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 _call to 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.0 line 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.py is 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
    (checkout v4→v7, setup-python v5→v7, deploy-pages v4→v5,
    configure-pages v5→v6, upload-pages-artifact v3→v5, login-action v3→v4,
    codeql-action v3→v4), clearing a seven-PR Dependabot backlog and the Node 20
    deprecation warning on every run. The breaking changes were read rather than
    assumed: checkout v7 blocks fork-PR checkout under pull_request_target /
    workflow_run and setup-python v7 removed the pip-install input — this repo
    uses neither. mkdocs-material docs-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

Choose a tag to compare

@dwgx dwgx released this 27 Jul 08:15

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-toolkit console commands; the wheel now includes the drive/MCP
    implementation instead of shipping only smartcli_core.
  • cwd and repeated KEY=VALUE environment controls for persistent sessions,
    machine-readable start/list/close output, and structured MCP snapshots.
  • visual_hash + wait_visual_change across 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 in test_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_change baseline 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 resize no longer kills the daemon and its live session
    (SystemExit escaped 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_hash is 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.sh refuses cleanly when no tmux client is attached instead of
    leaking tmux's no current client with a non-zero exit.
  • Real-session probes use the running Python interpreter with platform-correct
    quoting instead of assuming a python command 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 so uvx
    launch from the official MCP Registry works without extra flags.

SmartCLI v0.1.2

Choose a tag to compare

@dwgx dwgx released this 11 Jul 15:01

Correctness release — deep review + mutation-testing pass, every fix with a repro and a regression-lock test.

Install

pip install -U smartcli-toolkit

Fixed

  • readiness (#1)wait_ready/wait_until_stable no longer declare STABLE on a never-painted blank screen during a startup quiet-gap (optional blank_hash gate; default off = old behavior).
  • docs (#2) — quickstart marker >>> $ never matched pyte's space-padded lines; examples now use unanchored >>> .
  • PTY backend (#4)WinptyBackend.spawn resets 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), and fx Param int coercion (08/010 and ±-signed based literals).

Added

  • Regression-lock tests (test_readiness, test_degenerate_inputs, test_fx_contract 18×6, box_junction self-test) + unified tests/run_all.py.

Full notes: CHANGELOG.md

SmartCLI v0.1.0

Choose a tag to compare

@dwgx dwgx released this 08 Jul 17:44

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.txt

What'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) + pyte screen model + semantic snapshot + readiness sync
  • A 122-note knowledge graph, screenshot/AGENTCLI verification harnesses, MIT license, and CI.

Links

Cross-platform (Windows / macOS / Linux), pure standard library at the core.