deepy-cli 0.1.12__tar.gz → 0.1.13__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 (73) hide show
  1. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/PKG-INFO +2 -3
  2. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/README.md +1 -1
  3. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/pyproject.toml +1 -2
  4. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/__init__.py +1 -1
  5. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/local_command.py +82 -54
  6. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/__main__.py +0 -0
  7. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/cli.py +0 -0
  8. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/config/__init__.py +0 -0
  9. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/config/settings.py +0 -0
  10. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/__init__.py +0 -0
  11. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/AskUserQuestion.md +0 -0
  12. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/WebFetch.md +0 -0
  13. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/WebSearch.md +0 -0
  14. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/__init__.py +0 -0
  15. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/edit.md +0 -0
  16. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/modify.md +0 -0
  17. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/read.md +0 -0
  18. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/shell.md +0 -0
  19. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/data/tools/write.md +0 -0
  20. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/errors.py +0 -0
  21. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/__init__.py +0 -0
  22. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/agent.py +0 -0
  23. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/compaction.py +0 -0
  24. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/context.py +0 -0
  25. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/events.py +0 -0
  26. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/model_capabilities.py +0 -0
  27. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/provider.py +0 -0
  28. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/replay.py +0 -0
  29. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/runner.py +0 -0
  30. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/llm/thinking.py +0 -0
  31. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/prompts/__init__.py +0 -0
  32. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/prompts/compact.py +0 -0
  33. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/prompts/rules.py +0 -0
  34. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/prompts/runtime_context.py +0 -0
  35. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/prompts/system.py +0 -0
  36. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/prompts/tool_docs.py +0 -0
  37. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/sessions/__init__.py +0 -0
  38. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/sessions/jsonl.py +0 -0
  39. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/sessions/manager.py +0 -0
  40. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/skills.py +0 -0
  41. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/status.py +0 -0
  42. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/tools/__init__.py +0 -0
  43. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/tools/agents.py +0 -0
  44. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/tools/builtin.py +0 -0
  45. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/tools/file_state.py +0 -0
  46. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/tools/result.py +0 -0
  47. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/tools/shell_utils.py +0 -0
  48. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/__init__.py +0 -0
  49. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/app.py +0 -0
  50. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/ask_user_question.py +0 -0
  51. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/exit_summary.py +0 -0
  52. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/file_mentions.py +0 -0
  53. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/loading_text.py +0 -0
  54. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/markdown.py +0 -0
  55. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/message_view.py +0 -0
  56. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/model_picker.py +0 -0
  57. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/prompt_buffer.py +0 -0
  58. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/prompt_input.py +0 -0
  59. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/session_list.py +0 -0
  60. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/session_picker.py +0 -0
  61. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/slash_commands.py +0 -0
  62. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/styles.py +0 -0
  63. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/terminal.py +0 -0
  64. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/theme_picker.py +0 -0
  65. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/thinking_state.py +0 -0
  66. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/ui/welcome.py +0 -0
  67. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/update_check.py +0 -0
  68. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/usage.py +0 -0
  69. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/utils/__init__.py +0 -0
  70. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/utils/debug_logger.py +0 -0
  71. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/utils/error_logger.py +0 -0
  72. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/utils/json.py +0 -0
  73. {deepy_cli-0.1.12 → deepy_cli-0.1.13}/src/deepy/utils/notify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: deepy-cli
3
- Version: 0.1.12
3
+ Version: 0.1.13
4
4
  Summary: Deepy - Vibe coding for DeepSeek models in your terminal
5
5
  Keywords: deepseek,coding-agent,terminal,cli,agents
6
6
  Author: kirineko
@@ -17,7 +17,6 @@ Requires-Dist: openai>=2.26,<3
17
17
  Requires-Dist: orjson>=3.10,<4
18
18
  Requires-Dist: pydantic>=2.12,<3
19
19
  Requires-Dist: prompt-toolkit>=3.0,<4
20
- Requires-Dist: pywinpty>=2.0,<3 ; sys_platform == 'win32'
21
20
  Requires-Dist: rich>=13.9,<15
22
21
  Requires-Dist: tiktoken>=0.9,<1
23
22
  Requires-Dist: tomli-w>=1
@@ -79,7 +78,7 @@ of hiding tool calls behind chat text.
79
78
  - **Local command mode**: type `!cmd` to run a non-interactive local shell command
80
79
  without sending it to the model; the result is still saved as context.
81
80
  - **Cross-platform shell handling**: POSIX shell, PowerShell, cmd, Windows paths,
82
- UTF-8 output, CRLF editing, and pywinpty-backed local command mode.
81
+ UTF-8 output, CRLF editing, and non-interactive Windows local command mode.
83
82
 
84
83
  ## See It Work
85
84
 
@@ -50,7 +50,7 @@ of hiding tool calls behind chat text.
50
50
  - **Local command mode**: type `!cmd` to run a non-interactive local shell command
51
51
  without sending it to the model; the result is still saved as context.
52
52
  - **Cross-platform shell handling**: POSIX shell, PowerShell, cmd, Windows paths,
53
- UTF-8 output, CRLF editing, and pywinpty-backed local command mode.
53
+ UTF-8 output, CRLF editing, and non-interactive Windows local command mode.
54
54
 
55
55
  ## See It Work
56
56
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepy-cli"
3
- version = "0.1.12"
3
+ version = "0.1.13"
4
4
  description = "Deepy - Vibe coding for DeepSeek models in your terminal"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -23,7 +23,6 @@ dependencies = [
23
23
  "orjson>=3.10,<4",
24
24
  "pydantic>=2.12,<3",
25
25
  "prompt-toolkit>=3.0,<4",
26
- "pywinpty>=2.0,<3; sys_platform == 'win32'",
27
26
  "rich>=13.9,<15",
28
27
  "tiktoken>=0.9,<1",
29
28
  "tomli-w>=1",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.1.12"
3
+ __version__ = "0.1.13"
4
4
 
5
5
 
6
6
  def main() -> None:
@@ -4,6 +4,7 @@ import errno
4
4
  import contextlib
5
5
  import os
6
6
  import queue
7
+ import re
7
8
  import select
8
9
  import shutil
9
10
  import signal
@@ -29,6 +30,21 @@ DEFAULT_LOCAL_COMMAND_TIMEOUT_MS = 120_000
29
30
  DEFAULT_DISPLAY_OUTPUT_LIMIT = 30_000
30
31
  DEFAULT_CONTEXT_OUTPUT_LIMIT = 8_000
31
32
  _TRUNCATED_MARKER = "\n... output truncated ...\n"
33
+ _ANSI_CONTROL_RE = re.compile(
34
+ r"""
35
+ \x1b
36
+ (?:
37
+ \[[0-?]*[ -/]*[@-~]
38
+ |\][^\x07\x1b]*(?:\x07|\x1b\\)
39
+ |P[^\x1b]*(?:\x1b\\)
40
+ |_[^\x1b]*(?:\x1b\\)
41
+ |\^[^\x1b]*(?:\x1b\\)
42
+ |[@-Z\\-_]
43
+ )
44
+ """,
45
+ re.VERBOSE,
46
+ )
47
+ _TERMINAL_CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]")
32
48
 
33
49
 
34
50
  @dataclass(frozen=True)
@@ -97,7 +113,7 @@ def run_local_command(
97
113
  capture_limit = max(display_limit, context_limit) + len(_TRUNCATED_MARKER)
98
114
 
99
115
  if runtime.os_family == "windows":
100
- return _run_windows_pty(
116
+ return _run_windows_pipes(
101
117
  command,
102
118
  cwd=cwd,
103
119
  env=process_env,
@@ -155,6 +171,8 @@ def shell_tool_result_json(
155
171
  output: str | None = None,
156
172
  ) -> str:
157
173
  rendered_output = result.context_output if output is None else output
174
+ if result.os_family == "windows":
175
+ rendered_output = _sanitize_terminal_output(rendered_output)
158
176
  metadata = _shell_metadata(result)
159
177
  if result.ok:
160
178
  return ToolResult.ok_result("shell", rendered_output, metadata=metadata).to_json()
@@ -270,7 +288,7 @@ def _run_posix_pty(
270
288
  )
271
289
 
272
290
 
273
- def _run_windows_pty(
291
+ def _run_windows_pipes(
274
292
  command: str,
275
293
  *,
276
294
  cwd: Path,
@@ -284,70 +302,56 @@ def _run_windows_pty(
284
302
  started_at: float,
285
303
  should_interrupt: Callable[[], bool] | None,
286
304
  ) -> LocalCommandResult:
287
- try:
288
- from winpty import PtyProcess # type: ignore[import-not-found]
289
- except Exception:
290
- error = "Windows local command mode requires the pywinpty package."
291
- return _error_result(
292
- command,
293
- cwd=cwd,
294
- runtime=runtime,
295
- shell_path=shell_path,
296
- timeout_ms=timeout_ms,
297
- started_at=started_at,
298
- tty_mode="unavailable",
299
- error=error,
300
- )
301
-
302
- process = None
305
+ process: subprocess.Popen[bytes] | None = None
303
306
  timed_out = False
304
307
  interrupted = False
305
- captured = ""
308
+ captured = bytearray()
306
309
  capture_truncated = False
307
- output_queue: queue.Queue[str] = queue.Queue()
308
- stop_reader = threading.Event()
310
+ output_queue: queue.Queue[bytes] = queue.Queue()
309
311
  try:
310
- process = PtyProcess.spawn(
312
+ process = subprocess.Popen(
311
313
  [shell_path, *_shell_args(runtime, command)],
312
314
  cwd=str(cwd),
313
315
  env=env,
316
+ stdin=subprocess.DEVNULL,
317
+ stdout=subprocess.PIPE,
318
+ stderr=subprocess.STDOUT,
319
+ close_fds=True,
314
320
  )
315
- reader = threading.Thread(
316
- target=_read_windows_pty_output,
317
- args=(process, output_queue, stop_reader),
318
- daemon=True,
319
- )
321
+ reader = threading.Thread(target=_read_pipe_output, args=(process, output_queue), daemon=True)
320
322
  reader.start()
321
323
  deadline = started_at + timeout_ms / 1000
322
324
  while True:
323
- captured, drained_truncated = _drain_text_queue(
325
+ drained_truncated = _drain_bytes_queue(
324
326
  output_queue,
325
327
  captured,
326
328
  capture_limit,
327
329
  )
328
330
  capture_truncated = capture_truncated or drained_truncated
329
- if not process.isalive():
331
+ if process.poll() is not None:
330
332
  break
331
333
  if callable(should_interrupt) and should_interrupt():
332
334
  interrupted = True
333
- process.terminate(force=True)
335
+ _terminate_windows_process(process)
334
336
  break
335
337
  remaining = deadline - time.monotonic()
336
338
  if remaining <= 0:
337
339
  timed_out = True
338
- process.terminate(force=True)
340
+ _terminate_windows_process(process)
339
341
  break
340
342
  time.sleep(min(0.05, remaining))
341
- stop_reader.set()
342
343
  reader.join(timeout=0.2)
343
- captured, drained_truncated = _drain_text_queue(output_queue, captured, capture_limit)
344
- capture_truncated = capture_truncated or drained_truncated
345
- exit_code = process.exitstatus
346
- output = captured
344
+ capture_truncated = (
345
+ capture_truncated or _drain_bytes_queue(output_queue, captured, capture_limit)
346
+ )
347
+ if len(captured) >= capture_limit:
348
+ capture_truncated = True
349
+ exit_code = process.poll()
350
+ output = _sanitize_terminal_output(_decode_output(bytes(captured)))
347
351
  error = _command_error(exit_code, timed_out=timed_out, interrupted=interrupted)
348
352
  except Exception as exc:
349
353
  exit_code = None
350
- output = captured
354
+ output = _sanitize_terminal_output(_decode_output(bytes(captured)))
351
355
  error = str(exc)
352
356
 
353
357
  display_output, display_truncated = _limit_output(output, display_limit)
@@ -364,7 +368,7 @@ def _run_windows_pty(
364
368
  command_dialect=runtime.command_dialect,
365
369
  path_style=runtime.path_style,
366
370
  os_family=runtime.os_family,
367
- tty_mode="winpty",
371
+ tty_mode="pipe",
368
372
  duration_ms=_elapsed_ms(started_at),
369
373
  timeout_ms=timeout_ms,
370
374
  timed_out=timed_out,
@@ -376,36 +380,37 @@ def _run_windows_pty(
376
380
  )
377
381
 
378
382
 
379
- def _read_windows_pty_output(
380
- process: Any,
381
- output_queue: queue.Queue[str],
382
- stop_reader: threading.Event,
383
+ def _read_pipe_output(
384
+ process: subprocess.Popen[bytes],
385
+ output_queue: queue.Queue[bytes],
383
386
  ) -> None:
384
- while not stop_reader.is_set():
387
+ stream = process.stdout
388
+ if stream is None:
389
+ return
390
+ while True:
385
391
  try:
386
- chunk = process.read(4096)
392
+ chunk = stream.read1(4096)
387
393
  except Exception:
388
394
  return
389
- if chunk:
390
- output_queue.put(chunk)
391
- elif not process.isalive():
395
+ if not chunk:
392
396
  return
397
+ output_queue.put(chunk)
393
398
 
394
399
 
395
- def _drain_text_queue(
396
- output_queue: queue.Queue[str],
397
- captured: str,
400
+ def _drain_bytes_queue(
401
+ output_queue: queue.Queue[bytes],
402
+ captured: bytearray,
398
403
  capture_limit: int,
399
- ) -> tuple[str, bool]:
404
+ ) -> bool:
400
405
  truncated = False
401
406
  while True:
402
407
  try:
403
408
  chunk = output_queue.get_nowait()
404
409
  except queue.Empty:
405
- return captured, truncated
410
+ return truncated
406
411
  if len(captured) < capture_limit:
407
412
  remaining = capture_limit - len(captured)
408
- captured += chunk[:remaining]
413
+ captured.extend(chunk[:remaining])
409
414
  if len(chunk) > remaining:
410
415
  truncated = True
411
416
  else:
@@ -472,6 +477,20 @@ def _terminate_process(process: subprocess.Popen[bytes]) -> None:
472
477
  process.wait(timeout=0.2)
473
478
 
474
479
 
480
+ def _terminate_windows_process(process: subprocess.Popen[bytes]) -> None:
481
+ if process.poll() is not None:
482
+ return
483
+ with contextlib.suppress(OSError):
484
+ process.terminate()
485
+ try:
486
+ process.wait(timeout=0.2)
487
+ except subprocess.TimeoutExpired:
488
+ with contextlib.suppress(OSError):
489
+ process.kill()
490
+ with contextlib.suppress(OSError, subprocess.TimeoutExpired):
491
+ process.wait(timeout=0.2)
492
+
493
+
475
494
  def _limit_output(output: str, limit: int) -> tuple[str, bool]:
476
495
  if limit <= 0:
477
496
  return (_TRUNCATED_MARKER if output else ""), bool(output)
@@ -485,7 +504,16 @@ def _limit_output(output: str, limit: int) -> tuple[str, bool]:
485
504
 
486
505
 
487
506
  def _decode_output(output: bytes) -> str:
488
- return output.replace(b"\r\n", b"\n").replace(b"\r", b"\n").decode("utf-8", errors="replace")
507
+ return _normalize_line_endings(output.decode("utf-8", errors="replace"))
508
+
509
+
510
+ def _sanitize_terminal_output(output: str) -> str:
511
+ normalized = _normalize_line_endings(output)
512
+ return _TERMINAL_CONTROL_CHAR_RE.sub("", _ANSI_CONTROL_RE.sub("", normalized))
513
+
514
+
515
+ def _normalize_line_endings(output: str) -> str:
516
+ return output.replace("\r\n", "\n").replace("\r", "\n")
489
517
 
490
518
 
491
519
  def _command_error(exit_code: int | None, *, timed_out: bool, interrupted: bool) -> str | None:
File without changes