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.
Files changed (41) hide show
  1. {langstage_cli-0.6.22/langstage_cli.egg-info → langstage_cli-0.6.23}/PKG-INFO +1 -1
  2. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/cli.py +43 -14
  3. {langstage_cli-0.6.22 → langstage_cli-0.6.23/langstage_cli.egg-info}/PKG-INFO +1 -1
  4. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/pyproject.toml +1 -1
  5. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_quiet_output.py +38 -0
  6. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/LICENSE +0 -0
  7. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/README.md +0 -0
  8. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/deepagent_code/__init__.py +0 -0
  9. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/__init__.py +0 -0
  10. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/agui_stream.py +0 -0
  11. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli/config.py +0 -0
  12. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/SOURCES.txt +0 -0
  13. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/dependency_links.txt +0 -0
  14. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/entry_points.txt +0 -0
  15. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/requires.txt +0 -0
  16. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/langstage_cli.egg-info/top_level.txt +0 -0
  17. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/setup.cfg +0 -0
  18. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_agui_stream.py +0 -0
  19. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_async_mode.py +0 -0
  20. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_checkpointer.py +0 -0
  21. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_cli.py +0 -0
  22. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_cli_help.py +0 -0
  23. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_codeconfig.py +0 -0
  24. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_config.py +0 -0
  25. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_config_command.py +0 -0
  26. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_configurable.py +0 -0
  27. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_help_render.py +0 -0
  28. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_interrupt_non_tty.py +0 -0
  29. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_no_interactive_approve.py +0 -0
  30. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_rename_shim.py +0 -0
  31. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_show_config.py +0 -0
  32. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_spec_resolution.py +0 -0
  33. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_spinner_reply_survives.py +0 -0
  34. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_stream_mode.py +0 -0
  35. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_streaming_marker.py +0 -0
  36. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_terminal_width.py +0 -0
  37. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_turn_exit_and_render.py +0 -0
  38. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_unicode_console.py +0 -0
  39. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_verbose_command.py +0 -0
  40. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_verify_flag.py +0 -0
  41. {langstage_cli-0.6.22 → langstage_cli-0.6.23}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.22
3
+ Version: 0.6.23
4
4
  Summary: The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -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
- print(separator("dots"))
1514
- user_input = input(make_prompt()).strip()
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
- print() # Space before response
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
- print_timing(duration, verbose)
1575
- print()
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
- print_goodbye()
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). A single-shot run (a MESSAGE arg or -f/--file) or
1742
- # a --verify preflight that is piped stdout is not a TTY — auto-enables quiet
1743
- # so the consumer gets only the reply/verdict (no spinner or "Loaded" line);
1744
- # --quiet forces it in a terminal. Color is additionally stripped whenever stdout
1745
- # is not a TTY, matching well-behaved CLIs.
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 ((bool(message or prompt_file) or verify_agent) and not _is_tty)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langstage-cli
3
- Version: 0.6.22
3
+ Version: 0.6.23
4
4
  Summary: The terminal stage for your LangGraph agent — Claude Code-style CLI for any CompiledGraph
5
5
  Author-email: Kedar Dabhadkar <kdabhadk@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "langstage-cli"
7
- version = "0.6.22"
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