langstage-cli 0.6.22__tar.gz → 0.6.23__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langstage_cli-0.6.22/langstage_cli.egg-info → langstage_cli-0.6.23}/PKG-INFO +1 -1
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/cli.py +43 -14
- {langstage_cli-0.6.22 → langstage_cli-0.6.23/langstage_cli.egg-info}/PKG-INFO +1 -1
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/pyproject.toml +1 -1
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_quiet_output.py +38 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/LICENSE +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/README.md +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/deepagent_code/__init__.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/__init__.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/agui_stream.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/config.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/SOURCES.txt +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/dependency_links.txt +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/entry_points.txt +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/requires.txt +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/top_level.txt +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/setup.cfg +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_agui_stream.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_async_mode.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_checkpointer.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_cli.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_cli_help.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_codeconfig.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_config.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_config_command.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_configurable.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_help_render.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_interrupt_non_tty.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_no_interactive_approve.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_rename_shim.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_show_config.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_spec_resolution.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_spinner_reply_survives.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_stream_mode.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_streaming_marker.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_terminal_width.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_turn_exit_and_render.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_unicode_console.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_verbose_command.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_verify_flag.py +0 -0
- {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_workspace.py +0 -0
|
@@ -1507,11 +1507,22 @@ def run_conversation_loop(
|
|
|
1507
1507
|
if single_shot:
|
|
1508
1508
|
return had_error
|
|
1509
1509
|
|
|
1510
|
-
# Main conversation loop
|
|
1510
|
+
# Main conversation loop.
|
|
1511
|
+
#
|
|
1512
|
+
# This loop also consumes a message on piped (non-TTY) stdin — `echo "hi" |
|
|
1513
|
+
# langstage-cli` reads the line via input() and runs one turn, then EOF ends the
|
|
1514
|
+
# loop. That path is scriptable, not an interactive session, so when _QUIET is
|
|
1515
|
+
# set (via -q or the non-TTY auto-quiet above) every decorative element is
|
|
1516
|
+
# suppressed: the `····` separators, the `❯` prompt (input() with no prompt, so
|
|
1517
|
+
# no glyph and no bracketed-paste bytes), the `Nms` timing line, and the
|
|
1518
|
+
# `Goodbye!`. What reaches stdout is then exactly the reply the MESSAGE-arg path
|
|
1519
|
+
# emits. A real TTY session has _QUIET == False, so all of it renders unchanged.
|
|
1520
|
+
# (gh #93)
|
|
1511
1521
|
while True:
|
|
1512
1522
|
try:
|
|
1513
|
-
|
|
1514
|
-
|
|
1523
|
+
if not _QUIET:
|
|
1524
|
+
print(separator("dots"))
|
|
1525
|
+
user_input = input("" if _QUIET else make_prompt()).strip()
|
|
1515
1526
|
|
|
1516
1527
|
if not user_input:
|
|
1517
1528
|
continue
|
|
@@ -1565,22 +1576,32 @@ def run_conversation_loop(
|
|
|
1565
1576
|
if user_input.lower() == "exit":
|
|
1566
1577
|
break
|
|
1567
1578
|
|
|
1568
|
-
|
|
1579
|
+
if not _QUIET:
|
|
1580
|
+
print() # Space before response
|
|
1569
1581
|
|
|
1570
1582
|
# Run the agent (AG-UI is the only streaming path since langstage-core 1.0)
|
|
1571
1583
|
duration, _ = asyncio.run(
|
|
1572
1584
|
run_single_turn_agui(agui_agent, user_input, thread_id, interactive, verbose)
|
|
1573
1585
|
)
|
|
1574
|
-
|
|
1575
|
-
|
|
1586
|
+
if _QUIET:
|
|
1587
|
+
# Scriptable path (gh #93): cap the streamed reply with a single
|
|
1588
|
+
# newline and emit no `Nms` timing line — exactly what the quiet
|
|
1589
|
+
# MESSAGE-arg single-shot path does, so a piped one-liner produces
|
|
1590
|
+
# byte-identical output whether the message is an arg or on stdin.
|
|
1591
|
+
print()
|
|
1592
|
+
else:
|
|
1593
|
+
print_timing(duration, verbose)
|
|
1594
|
+
print()
|
|
1576
1595
|
|
|
1577
1596
|
except (EOFError, KeyboardInterrupt):
|
|
1578
1597
|
break
|
|
1579
1598
|
except Exception as err:
|
|
1580
1599
|
print(f"\n{RED}✗ Error: {err}{RESET}\n")
|
|
1581
1600
|
|
|
1582
|
-
# Print goodbye message
|
|
1583
|
-
|
|
1601
|
+
# Print goodbye message (interactive chrome — omitted on the scriptable/quiet
|
|
1602
|
+
# path so a piped consumer never sees a trailing `Goodbye!`, gh #93).
|
|
1603
|
+
if not _QUIET:
|
|
1604
|
+
print_goodbye()
|
|
1584
1605
|
|
|
1585
1606
|
|
|
1586
1607
|
@click.command()
|
|
@@ -1738,14 +1759,22 @@ def main(
|
|
|
1738
1759
|
except (AttributeError, ValueError): # non-reconfigurable stream
|
|
1739
1760
|
pass
|
|
1740
1761
|
|
|
1741
|
-
# Scriptable output (gh #53).
|
|
1742
|
-
#
|
|
1743
|
-
#
|
|
1744
|
-
#
|
|
1745
|
-
#
|
|
1762
|
+
# Scriptable output (gh #53, gh #93). Auto-enable quiet when stdout is not a TTY
|
|
1763
|
+
# AND the run is non-interactive input: an explicit single-shot (a MESSAGE arg,
|
|
1764
|
+
# -f/--file, or a --verify preflight) OR a message on piped stdin
|
|
1765
|
+
# (`echo "hi" | langstage-cli`). Piped stdin is non-interactive input — it is
|
|
1766
|
+
# scriptable too, so it must not leak the REPL's `····` rules, the `❯` prompt
|
|
1767
|
+
# (with bracketed-paste bytes), the `Nms` timing line or a trailing `Goodbye!`.
|
|
1768
|
+
# The gate is `stdin is not a tty`, so a LIVE terminal session (a human typing,
|
|
1769
|
+
# stdin IS a tty, no single-shot input) is never auto-quieted and its interactive
|
|
1770
|
+
# UX is unchanged; --quiet still forces quiet anywhere. Color is additionally
|
|
1771
|
+
# stripped whenever stdout is not a TTY, matching well-behaved CLIs.
|
|
1746
1772
|
_is_tty = _is_a_tty(sys.stdout)
|
|
1773
|
+
_stdin_is_tty = _is_a_tty(sys.stdin)
|
|
1747
1774
|
global _QUIET
|
|
1748
|
-
_QUIET = quiet or (
|
|
1775
|
+
_QUIET = quiet or (
|
|
1776
|
+
(bool(message or prompt_file) or verify_agent or not _stdin_is_tty) and not _is_tty
|
|
1777
|
+
)
|
|
1749
1778
|
if _QUIET or not _is_tty:
|
|
1750
1779
|
_disable_ansi()
|
|
1751
1780
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langstage-cli"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.23"
|
|
8
8
|
description = "The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -10,6 +10,8 @@ CliRunner's stdout is not a TTY, so ``invoke(main, [...])`` exercises exactly th
|
|
|
10
10
|
piped path these tests care about.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import re
|
|
14
|
+
|
|
13
15
|
from click.testing import CliRunner
|
|
14
16
|
|
|
15
17
|
from langstage_cli import cli as c
|
|
@@ -36,6 +38,42 @@ def test_quiet_flag_forces_clean_output(tmp_path, monkeypatch):
|
|
|
36
38
|
assert "\x1b[" not in r.output and "⏺" not in r.output, r.output
|
|
37
39
|
|
|
38
40
|
|
|
41
|
+
def test_piped_stdin_quiet_output_matches_the_message_arg_path(tmp_path, monkeypatch):
|
|
42
|
+
# gh #93: a message on piped stdin (`echo "hi" | langstage-cli --demo -q`) used to
|
|
43
|
+
# be routed through the interactive REPL, which honored neither -q nor the non-TTY
|
|
44
|
+
# auto-quiet — so it leaked the `····` separator rules, the `❯` prompt row (with
|
|
45
|
+
# `\x01\x02` bracketed-paste bytes), the `Nms` timing line, and a trailing `Goodbye!`
|
|
46
|
+
# around the reply (~380 bytes of chrome). It must now emit exactly what the
|
|
47
|
+
# MESSAGE-arg quiet path emits: only the agent's reply. CliRunner's stdin and stdout
|
|
48
|
+
# are both non-TTYs, so `input="hi\n"` with no MESSAGE arg reproduces the piped pipe.
|
|
49
|
+
monkeypatch.chdir(tmp_path)
|
|
50
|
+
arg = CliRunner().invoke(main, ["--demo", "-q", "hi"])
|
|
51
|
+
stdin = CliRunner().invoke(main, ["--demo", "-q"], input="hi\n")
|
|
52
|
+
assert arg.exit_code == 0 and stdin.exit_code == 0, (arg.output, stdin.output)
|
|
53
|
+
# Byte-for-byte identical reply streams — the stdin path carries no extra chrome.
|
|
54
|
+
assert stdin.stdout == arg.stdout, repr((arg.stdout, stdin.stdout))
|
|
55
|
+
assert stdin.stdout == "(demo agent) You said: hi\n", repr(stdin.stdout)
|
|
56
|
+
# And none of the specific chrome the issue grepped for reaches the reply stream.
|
|
57
|
+
assert re.search(r"\d+ms", stdin.stdout) is None, repr(stdin.stdout) # no timing line
|
|
58
|
+
assert "·" not in stdin.stdout, repr(stdin.stdout) # no `····` separator rule
|
|
59
|
+
assert "❯" not in stdin.stdout, repr(stdin.stdout) # no prompt row
|
|
60
|
+
assert "\x01" not in stdin.stdout and "\x02" not in stdin.stdout # no bracketed-paste bytes
|
|
61
|
+
assert "Goodbye" not in stdin.stdout, repr(stdin.stdout) # no trailing farewell
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_piped_stdin_auto_quiets_without_the_quiet_flag(tmp_path, monkeypatch):
|
|
65
|
+
# gh #93 / #53: the non-TTY auto-quiet must ALSO fire when the message arrives on
|
|
66
|
+
# stdin, with no -q — `echo "hi" | langstage-cli --demo | tool` is clean by default,
|
|
67
|
+
# exactly like the MESSAGE-arg auto-quiet. Piped stdin is non-interactive input, so
|
|
68
|
+
# the run is scriptable even without the explicit flag.
|
|
69
|
+
monkeypatch.chdir(tmp_path)
|
|
70
|
+
r = CliRunner().invoke(main, ["--demo"], input="hi\n")
|
|
71
|
+
assert r.exit_code == 0, r.output
|
|
72
|
+
assert r.stdout == "(demo agent) You said: hi\n", repr(r.stdout)
|
|
73
|
+
assert re.search(r"\d+ms", r.stdout) is None, repr(r.stdout)
|
|
74
|
+
assert "·" not in r.stdout and "❯" not in r.stdout and "Goodbye" not in r.stdout
|
|
75
|
+
|
|
76
|
+
|
|
39
77
|
def test_make_prompt_uses_disabled_color_globals_after_ansi_is_disabled():
|
|
40
78
|
c._disable_ansi()
|
|
41
79
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|