wcgw 4.1.0__tar.gz → 4.1.2__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.

Potentially problematic release.


This version of wcgw might be problematic. Click here for more details.

Files changed (132) hide show
  1. {wcgw-4.1.0 → wcgw-4.1.2}/PKG-INFO +4 -1
  2. {wcgw-4.1.0 → wcgw-4.1.2}/pyproject.toml +5 -1
  3. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/bash_state/bash_state.py +134 -3
  4. wcgw-4.1.2/src/wcgw/client/bash_state/parser/__init__.py +7 -0
  5. wcgw-4.1.2/src/wcgw/client/bash_state/parser/bash_statement_parser.py +181 -0
  6. wcgw-4.1.2/tests/test_bash_parser.py +80 -0
  7. wcgw-4.1.2/tests/test_bash_parser_complex.py +84 -0
  8. {wcgw-4.1.0 → wcgw-4.1.2}/tests/test_tools.py +18 -0
  9. {wcgw-4.1.0 → wcgw-4.1.2}/uv.lock +77 -1
  10. {wcgw-4.1.0 → wcgw-4.1.2}/.github/workflows/python-publish.yml +0 -0
  11. {wcgw-4.1.0 → wcgw-4.1.2}/.github/workflows/python-tests.yml +0 -0
  12. {wcgw-4.1.0 → wcgw-4.1.2}/.github/workflows/python-types.yml +0 -0
  13. {wcgw-4.1.0 → wcgw-4.1.2}/.gitignore +0 -0
  14. {wcgw-4.1.0 → wcgw-4.1.2}/.gitmodules +0 -0
  15. {wcgw-4.1.0 → wcgw-4.1.2}/.python-version +0 -0
  16. {wcgw-4.1.0 → wcgw-4.1.2}/.vscode/settings.json +0 -0
  17. {wcgw-4.1.0 → wcgw-4.1.2}/CLAUDE.md +0 -0
  18. {wcgw-4.1.0 → wcgw-4.1.2}/Dockerfile +0 -0
  19. {wcgw-4.1.0 → wcgw-4.1.2}/LICENSE +0 -0
  20. {wcgw-4.1.0 → wcgw-4.1.2}/README.md +0 -0
  21. {wcgw-4.1.0 → wcgw-4.1.2}/gpt_action_json_schema.json +0 -0
  22. {wcgw-4.1.0 → wcgw-4.1.2}/gpt_instructions.txt +0 -0
  23. {wcgw-4.1.0 → wcgw-4.1.2}/openai.md +0 -0
  24. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.git +0 -0
  25. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  26. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
  28. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
  29. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
  30. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
  31. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.gitignore +0 -0
  32. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/.python-version +0 -0
  33. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
  34. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
  35. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/LICENSE +0 -0
  36. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/README.md +0 -0
  37. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/RELEASE.md +0 -0
  38. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/SECURITY.md +0 -0
  39. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/README.md +0 -0
  40. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
  41. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
  42. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
  43. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
  44. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
  45. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
  46. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
  47. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
  48. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
  49. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
  50. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
  51. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
  52. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
  53. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
  54. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
  55. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
  56. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
  57. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
  58. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/pyproject.toml +0 -0
  59. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
  60. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
  61. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
  62. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
  63. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
  64. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
  65. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
  66. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
  67. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
  68. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
  69. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
  70. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
  71. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
  72. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
  73. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
  74. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
  75. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
  76. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
  77. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
  78. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
  79. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
  80. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
  81. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
  82. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
  83. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
  84. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
  85. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
  86. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
  87. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
  88. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
  89. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
  90. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
  91. {wcgw-4.1.0 → wcgw-4.1.2}/src/mcp_wcgw_fork/uv.lock +0 -0
  92. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/__init__.py +0 -0
  93. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/__init__.py +0 -0
  94. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/common.py +0 -0
  95. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/diff-instructions.txt +0 -0
  96. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/encoder/__init__.py +0 -0
  97. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/file_ops/diff_edit.py +0 -0
  98. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/file_ops/search_replace.py +0 -0
  99. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/mcp_server/Readme.md +0 -0
  100. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/mcp_server/__init__.py +0 -0
  101. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/mcp_server/server.py +0 -0
  102. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/memory.py +0 -0
  103. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/modes.py +0 -0
  104. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/repo_ops/display_tree.py +0 -0
  105. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/repo_ops/file_stats.py +0 -0
  106. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/repo_ops/path_prob.py +0 -0
  107. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
  108. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
  109. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/repo_ops/repo_context.py +0 -0
  110. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/tool_prompts.py +0 -0
  111. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/client/tools.py +0 -0
  112. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/py.typed +0 -0
  113. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/relay/client.py +0 -0
  114. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/relay/serve.py +0 -0
  115. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/relay/static/privacy.txt +0 -0
  116. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw/types_.py +0 -0
  117. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw_cli/__init__.py +0 -0
  118. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw_cli/__main__.py +0 -0
  119. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw_cli/anthropic_client.py +0 -0
  120. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw_cli/cli.py +0 -0
  121. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw_cli/openai_client.py +0 -0
  122. {wcgw-4.1.0 → wcgw-4.1.2}/src/wcgw_cli/openai_utils.py +0 -0
  123. {wcgw-4.1.0 → wcgw-4.1.2}/static/claude-ss.jpg +0 -0
  124. {wcgw-4.1.0 → wcgw-4.1.2}/static/computer-use.jpg +0 -0
  125. {wcgw-4.1.0 → wcgw-4.1.2}/static/example.jpg +0 -0
  126. {wcgw-4.1.0 → wcgw-4.1.2}/static/rocket-icon.png +0 -0
  127. {wcgw-4.1.0 → wcgw-4.1.2}/static/ss1.png +0 -0
  128. {wcgw-4.1.0 → wcgw-4.1.2}/static/workflow-demo.gif +0 -0
  129. {wcgw-4.1.0 → wcgw-4.1.2}/tests/test_edit.py +0 -0
  130. {wcgw-4.1.0 → wcgw-4.1.2}/tests/test_file_range_tracking.py +0 -0
  131. {wcgw-4.1.0 → wcgw-4.1.2}/tests/test_mcp_server.py +0 -0
  132. {wcgw-4.1.0 → wcgw-4.1.2}/tests/test_readfiles.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 4.1.0
3
+ Version: 4.1.2
4
4
  Summary: Shell and coding agent on claude and chatgpt
5
5
  Project-URL: Homepage, https://github.com/rusiaaman/wcgw
6
6
  Author-email: Aman Rusia <gapypi@arcfu.com>
@@ -11,6 +11,7 @@ Requires-Dist: fastapi>=0.115.0
11
11
  Requires-Dist: openai>=1.46.0
12
12
  Requires-Dist: petname>=2.6
13
13
  Requires-Dist: pexpect>=4.9.0
14
+ Requires-Dist: psutil>=7.0.0
14
15
  Requires-Dist: pydantic>=2.9.2
15
16
  Requires-Dist: pygit2>=1.16.0
16
17
  Requires-Dist: pyte>=0.8.2
@@ -20,6 +21,8 @@ Requires-Dist: semantic-version>=2.10.0
20
21
  Requires-Dist: syntax-checker>=0.3.0
21
22
  Requires-Dist: tokenizers>=0.21.0
22
23
  Requires-Dist: toml>=0.10.2
24
+ Requires-Dist: tree-sitter-bash>=0.23.3
25
+ Requires-Dist: tree-sitter>=0.24.0
23
26
  Requires-Dist: typer>=0.12.5
24
27
  Requires-Dist: uvicorn>=0.31.0
25
28
  Requires-Dist: websockets>=13.1
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }]
3
3
  name = "wcgw"
4
- version = "4.1.0"
4
+ version = "4.1.2"
5
5
  description = "Shell and coding agent on claude and chatgpt"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -23,6 +23,9 @@ dependencies = [
23
23
  "tokenizers>=0.21.0",
24
24
  "pygit2>=1.16.0",
25
25
  "syntax-checker>=0.3.0",
26
+ "psutil>=7.0.0",
27
+ "tree-sitter>=0.24.0",
28
+ "tree-sitter-bash>=0.23.3",
26
29
  ]
27
30
 
28
31
  [project.urls]
@@ -58,6 +61,7 @@ dev-dependencies = [
58
61
  "line-profiler>=4.2.0",
59
62
  "pytest-asyncio>=0.25.3",
60
63
  "types-pexpect>=4.9.0.20241208",
64
+ "types-psutil>=7.0.0.20250218",
61
65
  ]
62
66
 
63
67
  [tool.pytest.ini_options]
@@ -16,6 +16,7 @@ from typing import (
16
16
  )
17
17
 
18
18
  import pexpect
19
+ import psutil
19
20
  import pyte
20
21
 
21
22
  from ...types_ import (
@@ -30,6 +31,7 @@ from ...types_ import (
30
31
  )
31
32
  from ..encoder import EncoderDecoder
32
33
  from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
34
+ from .parser.bash_statement_parser import BashStatementParser
33
35
 
34
36
  PROMPT_CONST = "wcgw→" + " "
35
37
  PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
@@ -87,6 +89,116 @@ def check_if_screen_command_available() -> bool:
87
89
  return False
88
90
 
89
91
 
92
+ def get_wcgw_screen_sessions() -> list[str]:
93
+ """
94
+ Get a list of all WCGW screen session IDs.
95
+
96
+ Returns:
97
+ List of screen session IDs that match the wcgw pattern.
98
+ """
99
+ screen_sessions = []
100
+
101
+ try:
102
+ # Get list of all screen sessions
103
+ result = subprocess.run(
104
+ ["screen", "-ls"],
105
+ capture_output=True,
106
+ text=True,
107
+ check=False, # Don't raise exception on non-zero exit code
108
+ timeout=0.5,
109
+ )
110
+ output = result.stdout or result.stderr or ""
111
+
112
+ # Parse screen output to get session IDs
113
+ for line in output.splitlines():
114
+ line = line.strip()
115
+ if not line or not line[0].isdigit():
116
+ continue
117
+
118
+ # Extract session info (e.g., "1234.wcgw.123456 (Detached)")
119
+ session_parts = line.split()
120
+ if not session_parts:
121
+ continue
122
+
123
+ session_id = session_parts[0].strip()
124
+
125
+ # Check if it's a WCGW session
126
+ if ".wcgw." in session_id:
127
+ screen_sessions.append(session_id)
128
+ except Exception:
129
+ # If anything goes wrong, just return empty list
130
+ pass
131
+
132
+ return screen_sessions
133
+
134
+
135
+ def get_orphaned_wcgw_screens() -> list[str]:
136
+ """
137
+ Identify orphaned WCGW screen sessions where the parent process has PID 1
138
+ or doesn't exist.
139
+
140
+ Returns:
141
+ List of screen session IDs that are orphaned and match the wcgw pattern.
142
+ """
143
+ orphaned_screens = []
144
+
145
+ try:
146
+ # Get list of all WCGW screen sessions
147
+ screen_sessions = get_wcgw_screen_sessions()
148
+
149
+ for session_id in screen_sessions:
150
+ # Extract PID from session ID (first part before the dot)
151
+ try:
152
+ pid = int(session_id.split(".")[0])
153
+
154
+ # Check if process exists and if its parent is PID 1
155
+ try:
156
+ process = psutil.Process(pid)
157
+ parent_pid = process.ppid()
158
+
159
+ if parent_pid == 1:
160
+ # This is an orphaned process
161
+ orphaned_screens.append(session_id)
162
+ except psutil.NoSuchProcess:
163
+ # Process doesn't exist anymore, consider it orphaned
164
+ orphaned_screens.append(session_id)
165
+ except (ValueError, IndexError):
166
+ # Couldn't parse PID, skip
167
+ continue
168
+ except Exception:
169
+ # If anything goes wrong, just return empty list
170
+ pass
171
+
172
+ return orphaned_screens
173
+
174
+
175
+ def cleanup_orphaned_wcgw_screens(console: Console) -> None:
176
+ """
177
+ Clean up all orphaned WCGW screen sessions.
178
+
179
+ Args:
180
+ console: Console for logging.
181
+ """
182
+ orphaned_sessions = get_orphaned_wcgw_screens()
183
+
184
+ if not orphaned_sessions:
185
+ return
186
+
187
+ console.log(
188
+ f"Found {len(orphaned_sessions)} orphaned WCGW screen sessions to clean up"
189
+ )
190
+
191
+ for session in orphaned_sessions:
192
+ try:
193
+ subprocess.run(
194
+ ["screen", "-S", session, "-X", "quit"],
195
+ check=False,
196
+ timeout=CONFIG.timeout,
197
+ )
198
+ except Exception as e:
199
+ console.log(f"Failed to kill orphaned screen session: {session}\n{e}")
200
+
201
+
90
202
  def cleanup_all_screens_with_name(name: str, console: Console) -> None:
91
203
  """
92
204
  There could be in worst case multiple screens with same name, clear them if any.
@@ -269,6 +381,8 @@ class BashState:
269
381
  self.close_bg_expect_thread()
270
382
  if set_as_command is not None:
271
383
  self._last_command = set_as_command
384
+ # if s == "\n":
385
+ # return self._shell.sendcontrol("m")
272
386
  output = self._shell.send(s)
273
387
  return output
274
388
 
@@ -387,6 +501,11 @@ class BashState:
387
501
  self._last_command = ""
388
502
  # Ensure self._cwd exists
389
503
  os.makedirs(self._cwd, exist_ok=True)
504
+
505
+ # Clean up orphaned WCGW screen sessions
506
+ if check_if_screen_command_available():
507
+ cleanup_orphaned_wcgw_screens(self.console)
508
+
390
509
  try:
391
510
  self._shell, self._shell_id = start_shell(
392
511
  self._bash_command_mode.bash_mode == "restricted_mode",
@@ -817,10 +936,22 @@ def _execute_bash(
817
936
  raise ValueError(WAITING_INPUT_MESSAGE)
818
937
 
819
938
  command = command_data.command.strip()
939
+
940
+ # Check for multiple statements using the bash statement parser
820
941
  if "\n" in command:
821
- raise ValueError(
822
- "Command should not contain newline character in middle. Run only one command at a time."
823
- )
942
+ try:
943
+ parser = BashStatementParser()
944
+ statements = parser.parse_string(command)
945
+ if len(statements) > 1:
946
+ return (
947
+ "Error: Command contains multiple statements. Please run only one bash statement at a time.",
948
+ 0.0,
949
+ )
950
+ except Exception:
951
+ # Fall back to simple newline check if something goes wrong
952
+ raise ValueError(
953
+ "Command should not contain newline character in middle. Run only one command at a time."
954
+ )
824
955
 
825
956
  for i in range(0, len(command), 128):
826
957
  bash_state.send(command[i : i + 128], set_as_command=None)
@@ -0,0 +1,7 @@
1
+ """
2
+ Parser for bash statements using tree-sitter.
3
+
4
+ This module provides functionality to parse and identify individual bash statements.
5
+ """
6
+
7
+ from .bash_statement_parser import BashStatementParser, Statement
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bash Statement Parser
4
+
5
+ This script parses bash scripts and identifies individual statements using tree-sitter.
6
+ It correctly handles multi-line strings, command chains with && and ||, and semicolon-separated statements.
7
+ """
8
+
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from typing import Any, List, Optional
12
+
13
+ import tree_sitter_bash
14
+ from tree_sitter import Language, Parser
15
+
16
+
17
+ @dataclass
18
+ class Statement:
19
+ """A bash statement with its source code and position information."""
20
+
21
+ text: str
22
+ start_line: int
23
+ end_line: int
24
+ start_byte: int
25
+ end_byte: int
26
+ node_type: str
27
+ parent_type: Optional[str] = None
28
+
29
+ def __str__(self) -> str:
30
+ return self.text.strip()
31
+
32
+
33
+ class BashStatementParser:
34
+ def __init__(self) -> None:
35
+ # Use the precompiled bash language
36
+ self.language = Language(tree_sitter_bash.language())
37
+ self.parser = Parser(self.language)
38
+
39
+ def parse_file(self, file_path: str) -> List[Statement]:
40
+ """Parse a bash script file and return a list of statements."""
41
+ with open(file_path, "r", encoding="utf-8") as f:
42
+ content = f.read()
43
+ return self.parse_string(content)
44
+
45
+ def parse_string(self, content: str) -> List[Statement]:
46
+ """Parse a string containing bash script and return a list of statements."""
47
+ tree = self.parser.parse(bytes(content, "utf-8"))
48
+ root_node = tree.root_node
49
+
50
+ # For debugging: Uncomment to print the tree structure
51
+ # self._print_tree(root_node, content)
52
+
53
+ statements: List[Statement] = []
54
+ self._extract_statements(root_node, content, statements, None)
55
+
56
+ # Post-process statements to handle multi-line statements correctly
57
+ return self._post_process_statements(statements, content)
58
+
59
+ def _print_tree(self, node: Any, content: str, indent: str = "") -> None:
60
+ """Debug helper to print the entire syntax tree."""
61
+ node_text = content[node.start_byte : node.end_byte]
62
+ if len(node_text) > 40:
63
+ node_text = node_text[:37] + "..."
64
+ print(f"{indent}{node.type}: {repr(node_text)}")
65
+ for child in node.children:
66
+ self._print_tree(child, content, indent + " ")
67
+
68
+ def _extract_statements(
69
+ self,
70
+ node: Any,
71
+ content: str,
72
+ statements: List[Statement],
73
+ parent_type: Optional[str],
74
+ ) -> None:
75
+ """Recursively extract statements from the syntax tree."""
76
+ # Node types that represent bash statements
77
+ statement_node_types = {
78
+ # Basic statements
79
+ "command",
80
+ "variable_assignment",
81
+ "declaration_command",
82
+ "unset_command",
83
+ # Control flow statements
84
+ "for_statement",
85
+ "c_style_for_statement",
86
+ "while_statement",
87
+ "if_statement",
88
+ "case_statement",
89
+ # Function definition
90
+ "function_definition",
91
+ # Command chains and groups
92
+ "pipeline", # For command chains with | and |&
93
+ "list", # For command chains with && and ||
94
+ "compound_statement",
95
+ "subshell",
96
+ "redirected_statement",
97
+ }
98
+
99
+ # Create a Statement object for this node if it's a recognized statement type
100
+ if node.type in statement_node_types:
101
+ # Get the text of this statement
102
+ start_byte = node.start_byte
103
+ end_byte = node.end_byte
104
+ statement_text = content[start_byte:end_byte]
105
+
106
+ # Get line numbers
107
+ start_line = (
108
+ node.start_point[0] + 1
109
+ ) # tree-sitter uses 0-indexed line numbers
110
+ end_line = node.end_point[0] + 1
111
+
112
+ statements.append(
113
+ Statement(
114
+ text=statement_text,
115
+ start_line=start_line,
116
+ end_line=end_line,
117
+ start_byte=start_byte,
118
+ end_byte=end_byte,
119
+ node_type=node.type,
120
+ parent_type=parent_type,
121
+ )
122
+ )
123
+
124
+ # Update parent type for children
125
+ parent_type = node.type
126
+
127
+ # Recursively process all children
128
+ for child in node.children:
129
+ self._extract_statements(child, content, statements, parent_type)
130
+
131
+ def _post_process_statements(
132
+ self, statements: List[Statement], content: str
133
+ ) -> List[Statement]:
134
+ if not statements:
135
+ return []
136
+
137
+ # Filter out list statements that have been split
138
+ top_statements = []
139
+ for stmt in statements:
140
+ # Skip statements that are contained within others
141
+ is_contained = False
142
+ for other in statements:
143
+ if other is stmt:
144
+ continue
145
+
146
+ # Check if completely contained (except for lists we've split)
147
+ if other.node_type != "list" or ";" not in other.text:
148
+ if (
149
+ other.start_line <= stmt.start_line
150
+ and other.end_line >= stmt.end_line
151
+ and len(other.text) > len(stmt.text)
152
+ and stmt.text in other.text
153
+ ):
154
+ is_contained = True
155
+ break
156
+
157
+ if not is_contained:
158
+ top_statements.append(stmt)
159
+
160
+ # Sort by position in file for consistent output
161
+ top_statements.sort(key=lambda s: (s.start_line, s.text))
162
+
163
+ return top_statements
164
+
165
+
166
+ def main() -> None:
167
+ if len(sys.argv) < 2:
168
+ print("Usage: python bash_statement_parser.py <bash_script_file>")
169
+ sys.exit(1)
170
+
171
+ parser = BashStatementParser()
172
+ statements = parser.parse_file(sys.argv[1])
173
+
174
+ print(f"Found {len(statements)} statements:")
175
+ for i, stmt in enumerate(statements, 1):
176
+ print(f"\n--- Statement {i} (Lines {stmt.start_line}-{stmt.end_line}) ---")
177
+ print(stmt)
178
+
179
+
180
+ if __name__ == "__main__":
181
+ main()
@@ -0,0 +1,80 @@
1
+ """
2
+ Tests for the bash statement parser.
3
+ """
4
+
5
+ import pytest
6
+ from unittest.mock import patch
7
+
8
+ from wcgw.client.bash_state.parser.bash_statement_parser import BashStatementParser
9
+
10
+
11
+ def test_bash_statement_parser_basic():
12
+ """Test basic statement parsing."""
13
+ parser = BashStatementParser()
14
+
15
+ # Test single statement
16
+ statements = parser.parse_string("echo hello")
17
+ assert len(statements) == 1
18
+ assert statements[0].text == "echo hello"
19
+
20
+ # Test command with newlines inside string
21
+ statements = parser.parse_string('echo "hello\nworld"')
22
+ assert len(statements) == 1
23
+
24
+ # Test command with && chain
25
+ statements = parser.parse_string("echo hello && echo world")
26
+ assert len(statements) == 1
27
+
28
+ # Test command with || chain
29
+ statements = parser.parse_string("echo hello || echo world")
30
+ assert len(statements) == 1
31
+
32
+ # Test command with pipe
33
+ statements = parser.parse_string("echo hello | grep hello")
34
+ assert len(statements) == 1
35
+
36
+
37
+ def test_bash_statement_parser_multiple():
38
+ """Test multiple statement detection."""
39
+ parser = BashStatementParser()
40
+
41
+ # Test multiple statements on separate lines
42
+ statements = parser.parse_string("echo hello\necho world")
43
+ assert len(statements) == 2
44
+
45
+ # Test multiple statements with semicolons
46
+ statements = parser.parse_string("echo hello; echo world")
47
+ assert len(statements) == 2
48
+
49
+ # Test more complex case
50
+ statements = parser.parse_string("echo hello; echo world && echo again")
51
+ assert len(statements) == 2
52
+
53
+ # Test mixed separation
54
+ statements = parser.parse_string("echo a; echo b\necho c")
55
+ assert len(statements) == 3
56
+
57
+
58
+ def test_bash_statement_parser_complex():
59
+ """Test complex statement handling."""
60
+ parser = BashStatementParser()
61
+
62
+ # Test subshell
63
+ statements = parser.parse_string("(echo hello; echo world)")
64
+ assert len(statements) == 1
65
+
66
+ # Test braces
67
+ statements = parser.parse_string("{ echo hello; echo world; }")
68
+ assert len(statements) == 1
69
+
70
+ # Test semicolons in strings
71
+ statements = parser.parse_string('echo "hello;world"')
72
+ assert len(statements) == 1
73
+
74
+ # Test escaped semicolons
75
+ statements = parser.parse_string('echo hello\\; echo world')
76
+ assert len(statements) == 1
77
+
78
+ # Test quoted semicolons
79
+ statements = parser.parse_string("echo 'hello;world'")
80
+ assert len(statements) == 1
@@ -0,0 +1,84 @@
1
+ """
2
+ Tests specifically for complex bash parsing scenarios.
3
+ """
4
+
5
+ from wcgw.client.bash_state.parser.bash_statement_parser import BashStatementParser
6
+
7
+
8
+ def test_semicolon_lists():
9
+ """Test parsing of semicolon-separated commands."""
10
+ parser = BashStatementParser()
11
+
12
+ # Simple case: two commands separated by semicolon
13
+ statements = parser.parse_string("echo a; echo b")
14
+ assert len(statements) == 2
15
+ assert statements[0].text.strip() == "echo a"
16
+ assert statements[1].text.strip() == "echo b"
17
+
18
+ # Multiple semicolons
19
+ statements = parser.parse_string("echo a; echo b; echo c")
20
+ assert len(statements) == 3
21
+ assert statements[0].text.strip() == "echo a"
22
+ assert statements[1].text.strip() == "echo b"
23
+ assert statements[2].text.strip() == "echo c"
24
+
25
+ # Semicolons with whitespace
26
+ statements = parser.parse_string("echo a ; echo b")
27
+ assert len(statements) == 2
28
+ assert statements[0].text.strip() == "echo a"
29
+ assert statements[1].text.strip() == "echo b"
30
+
31
+
32
+ def test_bash_command_with_semicolons_in_quotes():
33
+ """Test that semicolons inside quotes don't split statements."""
34
+ parser = BashStatementParser()
35
+
36
+ # Semicolon in single quotes
37
+ statements = parser.parse_string("echo 'a;b'")
38
+ assert len(statements) == 1
39
+
40
+ # Semicolon in double quotes
41
+ statements = parser.parse_string('echo "a;b"')
42
+ assert len(statements) == 1
43
+
44
+ # Mixed quotes
45
+ statements = parser.parse_string("echo \"a;b\" ; echo 'c;d'")
46
+ assert len(statements) == 2
47
+
48
+
49
+ def test_complex_commands():
50
+ """Test complex command scenarios."""
51
+ parser = BashStatementParser()
52
+
53
+ # Command with redirection and semicolon
54
+ statements = parser.parse_string("cat > file.txt << EOF\ntest\nEOF\n; echo done")
55
+ assert len(statements) == 2
56
+
57
+ # Command with subshell and semicolon
58
+ statements = parser.parse_string("(cd /tmp && echo 'in tmp'); echo 'outside'")
59
+ assert len(statements) == 2
60
+
61
+ # Command with braces and semicolon
62
+ statements = parser.parse_string("{ echo a; echo b; }; echo c")
63
+ assert len(statements) == 2
64
+
65
+
66
+ def test_command_chaining():
67
+ """Test command chains are treated as a single statement."""
68
+ parser = BashStatementParser()
69
+
70
+ # AND chaining
71
+ statements = parser.parse_string("echo a && echo b")
72
+ assert len(statements) == 1
73
+
74
+ # OR chaining
75
+ statements = parser.parse_string("echo a || echo b")
76
+ assert len(statements) == 1
77
+
78
+ # Pipe chaining
79
+ statements = parser.parse_string("echo a | grep a")
80
+ assert len(statements) == 1
81
+
82
+ # Mixed chaining
83
+ statements = parser.parse_string("echo a && echo b || echo c")
84
+ assert len(statements) == 1
@@ -306,6 +306,24 @@ def test_bash_command(context: Context, temp_dir: str) -> None:
306
306
  assert isinstance(outputs[0], str)
307
307
  assert "hello world" in outputs[0]
308
308
 
309
+ # Test multiline
310
+ cmd = BashCommand(action_json=Command(command="echo 'hello \nworld'"))
311
+ outputs, _ = get_tool_output(
312
+ context, cmd, default_enc, 1.0, lambda x, y: ("", 0.0), None
313
+ )
314
+ assert len(outputs) == 1
315
+ assert isinstance(outputs[0], str)
316
+ assert "hello\nworld" in outputs[0]
317
+
318
+ # Multiple commands should raise exception
319
+ cmd = BashCommand(action_json=Command(command="echo 'hello'\necho world'"))
320
+ outputs, _ = get_tool_output(
321
+ context, cmd, default_enc, 1.0, lambda x, y: ("", 0.0), None
322
+ )
323
+ assert len(outputs) == 1
324
+ assert isinstance(outputs[0], str)
325
+ assert "Error: Command contains multiple statements" in outputs[0]
326
+
309
327
 
310
328
  def test_interaction_commands(context: Context, temp_dir: str) -> None:
311
329
  """Test the various interaction command types."""
@@ -645,6 +645,21 @@ wheels = [
645
645
  { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 },
646
646
  ]
647
647
 
648
+ [[package]]
649
+ name = "psutil"
650
+ version = "7.0.0"
651
+ source = { registry = "https://pypi.org/simple" }
652
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
653
+ wheels = [
654
+ { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 },
655
+ { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 },
656
+ { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 },
657
+ { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 },
658
+ { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 },
659
+ { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
660
+ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
661
+ ]
662
+
648
663
  [[package]]
649
664
  name = "ptyprocess"
650
665
  version = "0.7.0"
@@ -1104,6 +1119,50 @@ wheels = [
1104
1119
  { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359 },
1105
1120
  ]
1106
1121
 
1122
+ [[package]]
1123
+ name = "tree-sitter"
1124
+ version = "0.24.0"
1125
+ source = { registry = "https://pypi.org/simple" }
1126
+ sdist = { url = "https://files.pythonhosted.org/packages/a7/a2/698b9d31d08ad5558f8bfbfe3a0781bd4b1f284e89bde3ad18e05101a892/tree-sitter-0.24.0.tar.gz", hash = "sha256:abd95af65ca2f4f7eca356343391ed669e764f37748b5352946f00f7fc78e734", size = 168304 }
1127
+ wheels = [
1128
+ { url = "https://files.pythonhosted.org/packages/66/08/82aaf7cbea7286ee2a0b43e9b75cb93ac6ac132991b7d3c26ebe5e5235a3/tree_sitter-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de0fb7c18c6068cacff46250c0a0473e8fc74d673e3e86555f131c2c1346fb13", size = 140733 },
1129
+ { url = "https://files.pythonhosted.org/packages/8c/bd/1a84574911c40734d80327495e6e218e8f17ef318dd62bb66b55c1e969f5/tree_sitter-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7c9c89666dea2ce2b2bf98e75f429d2876c569fab966afefdcd71974c6d8538", size = 134243 },
1130
+ { url = "https://files.pythonhosted.org/packages/46/c1/c2037af2c44996d7bde84eb1c9e42308cc84b547dd6da7f8a8bea33007e1/tree_sitter-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddb113e6b8b3e3b199695b1492a47d87d06c538e63050823d90ef13cac585fd", size = 562030 },
1131
+ { url = "https://files.pythonhosted.org/packages/4c/aa/2fb4d81886df958e6ec7e370895f7106d46d0bbdcc531768326124dc8972/tree_sitter-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ea01a7003b88b92f7f875da6ba9d5d741e0c84bb1bd92c503c0eecd0ee6409", size = 575585 },
1132
+ { url = "https://files.pythonhosted.org/packages/e3/3c/5f997ce34c0d1b744e0f0c0757113bdfc173a2e3dadda92c751685cfcbd1/tree_sitter-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:464fa5b2cac63608915a9de8a6efd67a4da1929e603ea86abaeae2cb1fe89921", size = 578203 },
1133
+ { url = "https://files.pythonhosted.org/packages/d5/1f/f2bc7fa7c3081653ea4f2639e06ff0af4616c47105dbcc0746137da7620d/tree_sitter-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b1f3cbd9700e1fba0be2e7d801527e37c49fc02dc140714669144ef6ab58dce", size = 120147 },
1134
+ { url = "https://files.pythonhosted.org/packages/c0/4c/9add771772c4d72a328e656367ca948e389432548696a3819b69cdd6f41e/tree_sitter-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:f3f08a2ca9f600b3758792ba2406971665ffbad810847398d180c48cee174ee2", size = 108302 },
1135
+ { url = "https://files.pythonhosted.org/packages/e9/57/3a590f287b5aa60c07d5545953912be3d252481bf5e178f750db75572bff/tree_sitter-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14beeff5f11e223c37be7d5d119819880601a80d0399abe8c738ae2288804afc", size = 140788 },
1136
+ { url = "https://files.pythonhosted.org/packages/61/0b/fc289e0cba7dbe77c6655a4dd949cd23c663fd62a8b4d8f02f97e28d7fe5/tree_sitter-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26a5b130f70d5925d67b47db314da209063664585a2fd36fa69e0717738efaf4", size = 133945 },
1137
+ { url = "https://files.pythonhosted.org/packages/86/d7/80767238308a137e0b5b5c947aa243e3c1e3e430e6d0d5ae94b9a9ffd1a2/tree_sitter-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fc5c3c26d83c9d0ecb4fc4304fba35f034b7761d35286b936c1db1217558b4e", size = 564819 },
1138
+ { url = "https://files.pythonhosted.org/packages/bf/b3/6c5574f4b937b836601f5fb556b24804b0a6341f2eb42f40c0e6464339f4/tree_sitter-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:772e1bd8c0931c866b848d0369b32218ac97c24b04790ec4b0e409901945dd8e", size = 579303 },
1139
+ { url = "https://files.pythonhosted.org/packages/0a/f4/bd0ddf9abe242ea67cca18a64810f8af230fc1ea74b28bb702e838ccd874/tree_sitter-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:24a8dd03b0d6b8812425f3b84d2f4763322684e38baf74e5bb766128b5633dc7", size = 581054 },
1140
+ { url = "https://files.pythonhosted.org/packages/8c/1c/ff23fa4931b6ef1bbeac461b904ca7e49eaec7e7e5398584e3eef836ec96/tree_sitter-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9e8b1605ab60ed43803100f067eed71b0b0e6c1fb9860a262727dbfbbb74751", size = 120221 },
1141
+ { url = "https://files.pythonhosted.org/packages/b2/2a/9979c626f303177b7612a802237d0533155bf1e425ff6f73cc40f25453e2/tree_sitter-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:f733a83d8355fc95561582b66bbea92ffd365c5d7a665bc9ebd25e049c2b2abb", size = 108234 },
1142
+ { url = "https://files.pythonhosted.org/packages/61/cd/2348339c85803330ce38cee1c6cbbfa78a656b34ff58606ebaf5c9e83bd0/tree_sitter-0.24.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d4a6416ed421c4210f0ca405a4834d5ccfbb8ad6692d4d74f7773ef68f92071", size = 140781 },
1143
+ { url = "https://files.pythonhosted.org/packages/8b/a3/1ea9d8b64e8dcfcc0051028a9c84a630301290995cd6e947bf88267ef7b1/tree_sitter-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0992d483677e71d5c5d37f30dfb2e3afec2f932a9c53eec4fca13869b788c6c", size = 133928 },
1144
+ { url = "https://files.pythonhosted.org/packages/fe/ae/55c1055609c9428a4aedf4b164400ab9adb0b1bf1538b51f4b3748a6c983/tree_sitter-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57277a12fbcefb1c8b206186068d456c600dbfbc3fd6c76968ee22614c5cd5ad", size = 564497 },
1145
+ { url = "https://files.pythonhosted.org/packages/ce/d0/f2ffcd04882c5aa28d205a787353130cbf84b2b8a977fd211bdc3b399ae3/tree_sitter-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25fa22766d63f73716c6fec1a31ee5cf904aa429484256bd5fdf5259051ed74", size = 578917 },
1146
+ { url = "https://files.pythonhosted.org/packages/af/82/aebe78ea23a2b3a79324993d4915f3093ad1af43d7c2208ee90be9273273/tree_sitter-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d5d9537507e1c8c5fa9935b34f320bfec4114d675e028f3ad94f11cf9db37b9", size = 581148 },
1147
+ { url = "https://files.pythonhosted.org/packages/a1/b4/6b0291a590c2b0417cfdb64ccb8ea242f270a46ed429c641fbc2bfab77e0/tree_sitter-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:f58bb4956917715ec4d5a28681829a8dad5c342cafd4aea269f9132a83ca9b34", size = 120207 },
1148
+ { url = "https://files.pythonhosted.org/packages/a8/18/542fd844b75272630229c9939b03f7db232c71a9d82aadc59c596319ea6a/tree_sitter-0.24.0-cp313-cp313-win_arm64.whl", hash = "sha256:23641bd25dcd4bb0b6fa91b8fb3f46cc9f1c9f475efe4d536d3f1f688d1b84c8", size = 108232 },
1149
+ ]
1150
+
1151
+ [[package]]
1152
+ name = "tree-sitter-bash"
1153
+ version = "0.23.3"
1154
+ source = { registry = "https://pypi.org/simple" }
1155
+ sdist = { url = "https://files.pythonhosted.org/packages/7b/e0/1e73a17c5427dc62fc42e29f1e58b3a3c95a8fa314983a37f25a0c15be1f/tree_sitter_bash-0.23.3.tar.gz", hash = "sha256:7b15ed89a1ea8e3e3c2399758746413e464d4c1c3a6d3b75d643ae2bc2fb356b", size = 420078 }
1156
+ wheels = [
1157
+ { url = "https://files.pythonhosted.org/packages/99/4d/c3a1105be2386fd68f1fe8ab9dac5d0d24011d610af2904f48a836525a73/tree_sitter_bash-0.23.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c1ee7a46fcbfca9937d01056be756631762f53c5afdb8c4ab64eb9fed060896b", size = 194714 },
1158
+ { url = "https://files.pythonhosted.org/packages/3c/d0/e87e4569a82a29037c06694ed0116c60368edef2d7b822a38514e95360d8/tree_sitter_bash-0.23.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5a090118e887bf667d82ae445794906186216f5500e0d2cd58eb499f7502dc57", size = 204226 },
1159
+ { url = "https://files.pythonhosted.org/packages/c5/a2/216ae6fffbcb0fb831865477f53115930dae5ad1b5a16ed12259c8aaecea/tree_sitter_bash-0.23.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa4b5dde719291eea3a81b1f9ece6afeee2deadc2b2f769bee92f955da7595cf", size = 265254 },
1160
+ { url = "https://files.pythonhosted.org/packages/2e/89/5031979dc0be9946e999f790b4ceef42810a819508f77fa4017aad0dd5e2/tree_sitter_bash-0.23.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff7bffc3d594e7f1054de051e19df1b24082963598a175dda64083c6b3eea1a", size = 242476 },
1161
+ { url = "https://files.pythonhosted.org/packages/52/0a/af4df04efbf253a93cc869f710a351ab2462f218135277268865eb599f5d/tree_sitter_bash-0.23.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4427baccbd7549a2ebb1859b6d42cdab0739c05d53c2b3daad9cadc069a7b3f6", size = 230285 },
1162
+ { url = "https://files.pythonhosted.org/packages/46/86/b1ecfe46a4dbae96a6900a3aa19ceb8d15815b29d924d36f02026780c719/tree_sitter_bash-0.23.3-cp39-abi3-win_amd64.whl", hash = "sha256:525c5cce28a7c5624fb016ac8f3ae33d32968567b718f7878c6351229d2e8394", size = 196940 },
1163
+ { url = "https://files.pythonhosted.org/packages/e2/1e/cab42516fc72c136d388bfbefe34be9fc109ff5a1fbba3c30209970cd57e/tree_sitter_bash-0.23.3-cp39-abi3-win_arm64.whl", hash = "sha256:1f703d1bf6235355f6c900be64bf9f61fc4b1d0cfed6829b4eeb74a6b41ea910", size = 191919 },
1164
+ ]
1165
+
1107
1166
  [[package]]
1108
1167
  name = "typer"
1109
1168
  version = "0.15.1"
@@ -1128,6 +1187,15 @@ wheels = [
1128
1187
  { url = "https://files.pythonhosted.org/packages/37/c6/14c23e04bae6a83e051da86c1670684e59acadab333a8497384aa201defd/types_pexpect-4.9.0.20241208-py3-none-any.whl", hash = "sha256:1928f478528454f0fea3495c16cf1ee2e67fca5c9fe97d60b868ac48c1fd5633", size = 17085 },
1129
1188
  ]
1130
1189
 
1190
+ [[package]]
1191
+ name = "types-psutil"
1192
+ version = "7.0.0.20250218"
1193
+ source = { registry = "https://pypi.org/simple" }
1194
+ sdist = { url = "https://files.pythonhosted.org/packages/b5/7c/145600d30456e7ccbb499abcf718aab2bd830e604a0ae8eb32b67cd346a6/types_psutil-7.0.0.20250218.tar.gz", hash = "sha256:1e642cdafe837b240295b23b1cbd4691d80b08a07d29932143cbbae30eb0db9c", size = 19828 }
1195
+ wheels = [
1196
+ { url = "https://files.pythonhosted.org/packages/50/c8/f4365293408da4a9bcb1849d3efd8c60427cffff68cbb98ab1b81851d8bb/types_psutil-7.0.0.20250218-py3-none-any.whl", hash = "sha256:1447a30c282aafefcf8941ece854e1100eee7b0296a9d9be9977292f0269b121", size = 22763 },
1197
+ ]
1198
+
1131
1199
  [[package]]
1132
1200
  name = "types-toml"
1133
1201
  version = "0.10.8.20240310"
@@ -1170,7 +1238,7 @@ wheels = [
1170
1238
 
1171
1239
  [[package]]
1172
1240
  name = "wcgw"
1173
- version = "4.1.0"
1241
+ version = "4.1.1"
1174
1242
  source = { editable = "." }
1175
1243
  dependencies = [
1176
1244
  { name = "anthropic" },
@@ -1178,6 +1246,7 @@ dependencies = [
1178
1246
  { name = "openai" },
1179
1247
  { name = "petname" },
1180
1248
  { name = "pexpect" },
1249
+ { name = "psutil" },
1181
1250
  { name = "pydantic" },
1182
1251
  { name = "pygit2" },
1183
1252
  { name = "pyte" },
@@ -1202,7 +1271,10 @@ dev = [
1202
1271
  { name = "pytest" },
1203
1272
  { name = "pytest-asyncio" },
1204
1273
  { name = "pytest-cov" },
1274
+ { name = "tree-sitter" },
1275
+ { name = "tree-sitter-bash" },
1205
1276
  { name = "types-pexpect" },
1277
+ { name = "types-psutil" },
1206
1278
  { name = "types-toml" },
1207
1279
  ]
1208
1280
 
@@ -1213,6 +1285,7 @@ requires-dist = [
1213
1285
  { name = "openai", specifier = ">=1.46.0" },
1214
1286
  { name = "petname", specifier = ">=2.6" },
1215
1287
  { name = "pexpect", specifier = ">=4.9.0" },
1288
+ { name = "psutil", specifier = ">=7.0.0" },
1216
1289
  { name = "pydantic", specifier = ">=2.9.2" },
1217
1290
  { name = "pygit2", specifier = ">=1.16.0" },
1218
1291
  { name = "pyte", specifier = ">=0.8.2" },
@@ -1237,7 +1310,10 @@ dev = [
1237
1310
  { name = "pytest", specifier = ">=8.0.0" },
1238
1311
  { name = "pytest-asyncio", specifier = ">=0.25.3" },
1239
1312
  { name = "pytest-cov", specifier = ">=4.1.0" },
1313
+ { name = "tree-sitter", specifier = ">=0.24.0" },
1314
+ { name = "tree-sitter-bash", specifier = ">=0.23.3" },
1240
1315
  { name = "types-pexpect", specifier = ">=4.9.0.20241208" },
1316
+ { name = "types-psutil", specifier = ">=7.0.0.20250218" },
1241
1317
  { name = "types-toml", specifier = ">=0.10.8.20240310" },
1242
1318
  ]
1243
1319
 
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes