wcgw 4.1.0__py3-none-any.whl → 4.1.1__py3-none-any.whl

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.

@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 4.1.0
3
+ Version: 4.1.1
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
@@ -8,7 +8,9 @@ wcgw/client/memory.py,sha256=M0plOGE5WXTEAs7nVLg4eCpVhmSW94ckpg5D0ycWX5I,2927
8
8
  wcgw/client/modes.py,sha256=gXm0u5EGQuPYEPZnyAptdzusN0JzLMED_DyErojPE6s,10679
9
9
  wcgw/client/tool_prompts.py,sha256=zHJQHb_E44zA3rBIkAudibgP3Zbw0DjA-CKM8qlqmMU,4410
10
10
  wcgw/client/tools.py,sha256=JXk6t7iNtP2EK6mdTkMqCquGGQ6TtoIJ8AEhkSith14,43963
11
- wcgw/client/bash_state/bash_state.py,sha256=1i72oLJ73AHvj0XmK0kAKnksiZwxWdAg21K27loKmpo,34020
11
+ wcgw/client/bash_state/bash_state.py,sha256=hbLBRcBhFCGqHKwxl9b3R1XXAvBLm0P7N7iYpDOh1CM,38190
12
+ wcgw/client/bash_state/parser/__init__.py,sha256=AnlNSmoQTSoqqlLOLX4P1uXfzc5VGeCGJsGgtisq2zE,207
13
+ wcgw/client/bash_state/parser/bash_statement_parser.py,sha256=9a8vPO1r3_tXmaAcubTQ5UY-NseWlalgm8LZA17LXuY,6058
12
14
  wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
13
15
  wcgw/client/file_ops/diff_edit.py,sha256=eNxFRmVsBfY0ISwfsV5s8rlMwMxTlm6ko150iFFCsT8,18525
14
16
  wcgw/client/file_ops/search_replace.py,sha256=TaIPDqjgmTo4oghhO3zIFklq5JjAbx_aPHJ7yEgvDh4,6854
@@ -52,8 +54,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
52
54
  mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
53
55
  mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
54
56
  mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
55
- wcgw-4.1.0.dist-info/METADATA,sha256=6v32rPTVYTPeEGY9F0miFZRknGe31eeN0bZKWskoSW8,14916
56
- wcgw-4.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
57
- wcgw-4.1.0.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
58
- wcgw-4.1.0.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
59
- wcgw-4.1.0.dist-info/RECORD,,
57
+ wcgw-4.1.1.dist-info/METADATA,sha256=0uPUOufx9j1789XQHkl43hTwvJ5FD8QSX83bQRmI2Q8,14945
58
+ wcgw-4.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
59
+ wcgw-4.1.1.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
60
+ wcgw-4.1.1.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
61
+ wcgw-4.1.1.dist-info/RECORD,,
File without changes