wcgw 3.0.1rc1__tar.gz → 3.0.1rc2__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 (124) hide show
  1. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/PKG-INFO +1 -1
  2. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/pyproject.toml +1 -1
  3. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/bash_state/bash_state.py +137 -46
  4. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/file_ops/diff_edit.py +52 -1
  5. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/mcp_server/server.py +1 -1
  6. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/tools.py +4 -2
  7. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/relay/client.py +1 -1
  8. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/anthropic_client.py +1 -2
  9. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/openai_client.py +1 -1
  10. wcgw-3.0.1rc2/tests/test_edit.py +450 -0
  11. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/uv.lock +1 -1
  12. wcgw-3.0.1rc1/tests/test_edit.py +0 -269
  13. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.github/workflows/python-publish.yml +0 -0
  14. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.github/workflows/python-tests.yml +0 -0
  15. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.github/workflows/python-types.yml +0 -0
  16. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.gitignore +0 -0
  17. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.gitmodules +0 -0
  18. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.python-version +0 -0
  19. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.vscode/settings.json +0 -0
  20. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/Dockerfile +0 -0
  21. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/LICENSE +0 -0
  22. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/README.md +0 -0
  23. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/gpt_action_json_schema.json +0 -0
  24. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/gpt_instructions.txt +0 -0
  25. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/openai.md +0 -0
  26. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.git +0 -0
  27. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  29. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
  30. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
  31. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
  32. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
  33. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.gitignore +0 -0
  34. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.python-version +0 -0
  35. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
  36. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
  37. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/LICENSE +0 -0
  38. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/README.md +0 -0
  39. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/RELEASE.md +0 -0
  40. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/SECURITY.md +0 -0
  41. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/README.md +0 -0
  42. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
  43. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
  44. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
  45. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
  46. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
  47. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
  48. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
  49. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
  50. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
  51. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
  52. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
  53. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
  54. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
  55. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
  56. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
  57. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
  58. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
  59. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
  60. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/pyproject.toml +0 -0
  61. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
  62. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
  63. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
  64. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
  65. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
  66. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
  67. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
  68. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
  69. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
  70. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
  71. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
  72. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
  73. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
  74. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
  75. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
  76. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
  77. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
  78. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
  79. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
  80. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
  81. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
  82. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
  83. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
  84. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
  85. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
  86. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
  87. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
  88. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
  89. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
  90. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
  91. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
  92. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
  93. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/uv.lock +0 -0
  94. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/__init__.py +0 -0
  95. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/__init__.py +0 -0
  96. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/common.py +0 -0
  97. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/diff-instructions.txt +0 -0
  98. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/encoder/__init__.py +0 -0
  99. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/file_ops/search_replace.py +0 -0
  100. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/mcp_server/Readme.md +0 -0
  101. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/mcp_server/__init__.py +0 -0
  102. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/memory.py +0 -0
  103. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/modes.py +0 -0
  104. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/display_tree.py +0 -0
  105. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/path_prob.py +0 -0
  106. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
  107. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
  108. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/repo_context.py +0 -0
  109. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/tool_prompts.py +0 -0
  110. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/py.typed +0 -0
  111. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/relay/serve.py +0 -0
  112. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/relay/static/privacy.txt +0 -0
  113. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/types_.py +0 -0
  114. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/__init__.py +0 -0
  115. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/__main__.py +0 -0
  116. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/cli.py +0 -0
  117. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/openai_utils.py +0 -0
  118. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/claude-ss.jpg +0 -0
  119. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/computer-use.jpg +0 -0
  120. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/example.jpg +0 -0
  121. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/rocket-icon.png +0 -0
  122. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/ss1.png +0 -0
  123. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/tests/test_mcp_server.py +0 -0
  124. {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 3.0.1rc1
3
+ Version: 3.0.1rc2
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>
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }]
3
3
  name = "wcgw"
4
- version = "3.0.1rc1"
4
+ version = "3.0.1rc2"
5
5
  description = "Shell and coding agent on claude and chatgpt"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -6,7 +6,15 @@ import threading
6
6
  import time
7
7
  import traceback
8
8
  from dataclasses import dataclass
9
- from typing import Any, Literal, Optional
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Concatenate,
13
+ Literal,
14
+ Optional,
15
+ ParamSpec,
16
+ TypeVar,
17
+ )
10
18
 
11
19
  import pexpect
12
20
  import pyte
@@ -63,7 +71,7 @@ def get_tmpdir() -> str:
63
71
  timeout=CONFIG.timeout,
64
72
  ).strip()
65
73
  return result
66
- except subprocess.CalledProcessError:
74
+ except (subprocess.CalledProcessError, FileNotFoundError):
67
75
  return "//tmp"
68
76
  except Exception:
69
77
  return ""
@@ -98,6 +106,8 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
98
106
  except subprocess.CalledProcessError as e:
99
107
  # When no screens exist, screen may return a non-zero exit code.
100
108
  output = (e.stdout or "") + (e.stderr or "")
109
+ except FileNotFoundError:
110
+ return
101
111
 
102
112
  sessions_to_kill = []
103
113
 
@@ -121,7 +131,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
121
131
  check=True,
122
132
  timeout=CONFIG.timeout,
123
133
  )
124
- except subprocess.CalledProcessError:
134
+ except (subprocess.CalledProcessError, FileNotFoundError):
125
135
  console.log(f"Failed to kill screen session: {session}")
126
136
 
127
137
 
@@ -176,11 +186,9 @@ def start_shell(
176
186
  shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
177
187
  shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
178
188
 
179
- shell.sendline(f"screen -q -s /bin/bash -S {shellid}")
189
+ shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
180
190
  shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
181
191
 
182
- console.log(f"Entering screen session, name: {shellid}")
183
-
184
192
  shell.sendline("stty -icanon -echo")
185
193
  shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
186
194
 
@@ -217,7 +225,37 @@ def render_terminal_output(text: str) -> list[str]:
217
225
  return lines
218
226
 
219
227
 
228
+ P = ParamSpec("P")
229
+ R = TypeVar("R")
230
+
231
+
232
+ def requires_shell(
233
+ func: Callable[Concatenate["BashState", "pexpect.spawn[str]", P], R],
234
+ ) -> Callable[Concatenate["BashState", P], R]:
235
+ def wrapper(self: "BashState", /, *args: P.args, **kwargs: P.kwargs) -> R:
236
+ if not self._shell_loading.is_set():
237
+ if not self._shell_loading.wait(timeout=CONFIG.timeout):
238
+ raise RuntimeError("Shell initialization timeout")
239
+
240
+ if self._shell_error:
241
+ raise RuntimeError(f"Shell failed to initialize: {self._shell_error}.")
242
+
243
+ if not self._shell:
244
+ raise RuntimeError("Shell not initialized")
245
+
246
+ return func(self, self._shell, *args, **kwargs)
247
+
248
+ return wrapper
249
+
250
+
220
251
  class BashState:
252
+ _shell: Optional["pexpect.spawn[str]"]
253
+ _shell_id: Optional[str]
254
+ _shell_lock: threading.Lock
255
+ _shell_loading: threading.Event
256
+ _shell_error: Optional[Exception]
257
+ _use_screen: bool
258
+
221
259
  def __init__(
222
260
  self,
223
261
  console: Console,
@@ -243,34 +281,63 @@ class BashState:
243
281
  self._prompt = PROMPT_CONST
244
282
  self._bg_expect_thread: Optional[threading.Thread] = None
245
283
  self._bg_expect_thread_stop_event = threading.Event()
246
- self._init_shell(use_screen)
247
-
248
- def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
284
+ self._shell = None
285
+ self._shell_id = None
286
+ self._shell_lock = threading.Lock()
287
+ self._shell_loading = threading.Event()
288
+ self._shell_error = None
289
+ self._use_screen = use_screen
290
+ self._start_shell_loading()
291
+
292
+ def _start_shell_loading(self) -> None:
293
+ def load_shell() -> None:
294
+ try:
295
+ with self._shell_lock:
296
+ if self._shell is not None:
297
+ return
298
+ self._init_shell()
299
+ except Exception as e:
300
+ self._shell_error = e
301
+ finally:
302
+ self._shell_loading.set()
303
+
304
+ threading.Thread(target=load_shell).start()
305
+
306
+ @requires_shell
307
+ def expect(
308
+ self, shell: "pexpect.spawn[str]", pattern: Any, timeout: Optional[float] = -1
309
+ ) -> int:
249
310
  self.close_bg_expect_thread()
250
- return self._shell.expect(pattern, timeout)
311
+ return shell.expect(pattern, timeout)
251
312
 
252
- def send(self, s: str | bytes) -> int:
253
- output = self._shell.send(s)
313
+ @requires_shell
314
+ def send(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
315
+ output = shell.send(s)
254
316
  self.run_bg_expect_thread()
255
317
  return output
256
318
 
257
- def sendline(self, s: str | bytes) -> int:
258
- output = self._shell.sendline(s)
319
+ @requires_shell
320
+ def sendline(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
321
+ output = shell.sendline(s)
259
322
  self.run_bg_expect_thread()
260
323
  return output
261
324
 
262
325
  @property
263
- def linesep(self) -> Any:
264
- return self._shell.linesep
326
+ @requires_shell
327
+ def linesep(self, shell: "pexpect.spawn[str]") -> Any:
328
+ return shell.linesep
265
329
 
266
- def sendintr(self) -> None:
267
- self._shell.sendintr()
330
+ @requires_shell
331
+ def sendintr(self, shell: "pexpect.spawn[str]") -> None:
332
+ shell.sendintr()
268
333
 
269
334
  @property
270
- def before(self) -> Optional[str]:
271
- return self._shell.before
335
+ @requires_shell
336
+ def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
337
+ return shell.before
272
338
 
273
- def run_bg_expect_thread(self) -> None:
339
+ @requires_shell
340
+ def run_bg_expect_thread(self, shell: "pexpect.spawn[str]") -> None:
274
341
  """
275
342
  Run background expect thread for handling shell interactions.
276
343
  """
@@ -279,7 +346,7 @@ class BashState:
279
346
  while True:
280
347
  if self._bg_expect_thread_stop_event.is_set():
281
348
  break
282
- output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
349
+ output = shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
283
350
  if output == 0:
284
351
  break
285
352
 
@@ -300,8 +367,13 @@ class BashState:
300
367
 
301
368
  def cleanup(self) -> None:
302
369
  self.close_bg_expect_thread()
303
- self._shell.close(True)
304
- cleanup_all_screens_with_name(self._shell_id, self.console)
370
+ with self._shell_lock:
371
+ if self._shell:
372
+ self._shell.close(True)
373
+ if self._shell_id:
374
+ cleanup_all_screens_with_name(self._shell_id, self.console)
375
+ self._shell = None
376
+ self._shell_id = None
305
377
 
306
378
  def __enter__(self) -> "BashState":
307
379
  return self
@@ -325,26 +397,34 @@ class BashState:
325
397
  def write_if_empty_mode(self) -> WriteIfEmptyMode:
326
398
  return self._write_if_empty_mode
327
399
 
328
- def ensure_env_and_bg_jobs(self) -> Optional[int]:
400
+ @requires_shell
401
+ def ensure_env_and_bg_jobs(self, _: "pexpect.spawn[str]") -> Optional[int]:
402
+ return self._ensure_env_and_bg_jobs()
403
+
404
+ def _ensure_env_and_bg_jobs(self) -> Optional[int]:
405
+ # Do not add @requires_shell decorator here, as it will cause deadlock
406
+
407
+ self.close_bg_expect_thread()
408
+ assert self._shell is not None, "Bad state, shell is not initialized"
329
409
  if self._prompt != PROMPT_CONST:
330
410
  return None
331
411
  quick_timeout = 0.2 if not self.over_screen else 1
332
412
  # First reset the prompt in case venv was sourced or other reasons.
333
- self.sendline(f"export PS1={self._prompt}")
334
- self.expect(self._prompt, timeout=quick_timeout)
413
+ self._shell.sendline(f"export PS1={self._prompt}")
414
+ self._shell.expect(self._prompt, timeout=quick_timeout)
335
415
  # Reset echo also if it was enabled
336
- self.sendline("stty -icanon -echo")
337
- self.expect(self._prompt, timeout=quick_timeout)
338
- self.sendline("set +o pipefail")
339
- self.expect(self._prompt, timeout=quick_timeout)
340
- self.sendline("export GIT_PAGER=cat PAGER=cat")
341
- self.expect(self._prompt, timeout=quick_timeout)
342
- self.sendline("jobs | wc -l")
416
+ self._shell.sendline("stty -icanon -echo")
417
+ self._shell.expect(self._prompt, timeout=quick_timeout)
418
+ self._shell.sendline("set +o pipefail")
419
+ self._shell.expect(self._prompt, timeout=quick_timeout)
420
+ self._shell.sendline("export GIT_PAGER=cat PAGER=cat")
421
+ self._shell.expect(self._prompt, timeout=quick_timeout)
422
+ self._shell.sendline("jobs | wc -l")
343
423
  before = ""
344
424
  counts = 0
345
425
  while not _is_int(before): # Consume all previous output
346
426
  try:
347
- self.expect(self._prompt, timeout=quick_timeout)
427
+ self._shell.expect(self._prompt, timeout=quick_timeout)
348
428
  except pexpect.TIMEOUT:
349
429
  self.console.print(f"Couldn't get exit code, before: {before}")
350
430
  raise
@@ -366,7 +446,7 @@ class BashState:
366
446
  except ValueError:
367
447
  raise ValueError(f"Malformed output: {before}")
368
448
 
369
- def _init_shell(self, use_screen: bool) -> None:
449
+ def _init_shell(self) -> None:
370
450
  self._prompt = PROMPT_CONST
371
451
  self._state: Literal["repl"] | datetime.datetime = "repl"
372
452
  self._is_in_docker: Optional[str] = ""
@@ -377,9 +457,9 @@ class BashState:
377
457
  self._bash_command_mode.bash_mode == "restricted_mode",
378
458
  self._cwd,
379
459
  self.console,
380
- over_screen=use_screen,
460
+ over_screen=self._use_screen,
381
461
  )
382
- self.over_screen = use_screen
462
+ self.over_screen = self._use_screen
383
463
  except Exception as e:
384
464
  if not isinstance(e, ValueError):
385
465
  self.console.log(traceback.format_exc())
@@ -394,9 +474,7 @@ class BashState:
394
474
  self.over_screen = False
395
475
 
396
476
  self._pending_output = ""
397
-
398
- # Get exit info to ensure shell is ready
399
- self.ensure_env_and_bg_jobs()
477
+ self._ensure_env_and_bg_jobs()
400
478
 
401
479
  def set_pending(self, last_pending_output: str) -> None:
402
480
  if not isinstance(self._state, datetime.datetime):
@@ -429,10 +507,11 @@ class BashState:
429
507
  def prompt(self) -> str:
430
508
  return self._prompt
431
509
 
432
- def update_cwd(self) -> str:
433
- self.sendline("pwd")
434
- self.expect(self._prompt, timeout=0.2)
435
- before_val = self._shell.before
510
+ @requires_shell
511
+ def update_cwd(self, shell: "pexpect.spawn[str]") -> str:
512
+ shell.sendline("pwd")
513
+ shell.expect(self._prompt, timeout=0.2)
514
+ before_val = shell.before
436
515
  if not isinstance(before_val, str):
437
516
  before_val = str(before_val)
438
517
  before_lines = render_terminal_output(before_val)
@@ -442,7 +521,9 @@ class BashState:
442
521
 
443
522
  def reset_shell(self) -> None:
444
523
  self.cleanup()
445
- self._init_shell(True)
524
+ self._shell_loading.clear()
525
+ self._shell_error = None
526
+ self._start_shell_loading()
446
527
 
447
528
  def serialize(self) -> dict[str, Any]:
448
529
  """Serialize BashState to a dictionary for saving"""
@@ -476,6 +557,16 @@ class BashState:
476
557
  cwd: str,
477
558
  ) -> None:
478
559
  """Create a new BashState instance from a serialized state dictionary"""
560
+ if (
561
+ self._bash_command_mode == bash_command_mode
562
+ and ((self._cwd == cwd) or not cwd)
563
+ and (self._file_edit_mode == file_edit_mode)
564
+ and (self._write_if_empty_mode == write_if_empty_mode)
565
+ and (self._mode == mode)
566
+ and (self._whitelist_for_overwrite == set(whitelist_for_overwrite))
567
+ ):
568
+ # No need to reset shell if the state is the same
569
+ return
479
570
  self._bash_command_mode = bash_command_mode
480
571
  self._cwd = cwd or self._cwd
481
572
  self._file_edit_mode = file_edit_mode
@@ -102,6 +102,8 @@ def line_process_max_space_tolerance(line: str) -> str:
102
102
  return re.sub(r"\s", "", line)
103
103
 
104
104
 
105
+ REMOVE_INDENTATION = "Warning: matching after removing all spaces in lines."
106
+
105
107
  DEFAULT_TOLERANCES = [
106
108
  Tolerance(
107
109
  line_process=str.rstrip,
@@ -119,11 +121,50 @@ DEFAULT_TOLERANCES = [
119
121
  line_process=line_process_max_space_tolerance,
120
122
  severity_cat="WARNING",
121
123
  score_multiplier=50,
122
- error_name="Warning: matching after removing all spaces in lines.",
124
+ error_name=REMOVE_INDENTATION,
123
125
  ),
124
126
  ]
125
127
 
126
128
 
129
+ def fix_indentation(
130
+ matched_lines: list[str], searched_lines: list[str], replaced_lines: list[str]
131
+ ) -> list[str]:
132
+ if not matched_lines or not searched_lines or not replaced_lines:
133
+ return replaced_lines
134
+
135
+ def get_indentation(line: str) -> str:
136
+ match = re.match(r"^(\s*)", line)
137
+ assert match
138
+ return match.group(0)
139
+
140
+ matched_indents = [get_indentation(line) for line in matched_lines if line.strip()]
141
+ searched_indents = [
142
+ get_indentation(line) for line in searched_lines if line.strip()
143
+ ]
144
+ if len(matched_indents) != len(searched_indents):
145
+ return replaced_lines
146
+ diffs: list[int] = [
147
+ len(searched) - len(matched)
148
+ for matched, searched in zip(matched_indents, searched_indents)
149
+ ]
150
+ if not all(diff == diffs[0] for diff in diffs):
151
+ return replaced_lines
152
+ if diffs[0] == 0:
153
+ return replaced_lines
154
+
155
+ # At this point we have same number of non-empty lines and the same indentation difference
156
+ # We can now adjust the indentation of the replaced lines
157
+ def adjust_indentation(line: str, diff: int) -> str:
158
+ if diff < 0:
159
+ return matched_indents[0][:-diff] + line
160
+ return line[diff:]
161
+
162
+ if diffs[0] > 0:
163
+ if not (all(not line[: diffs[0]].strip() for line in replaced_lines)):
164
+ return replaced_lines
165
+ return [adjust_indentation(line, diffs[0]) for line in replaced_lines]
166
+
167
+
127
168
  def remove_leading_trailing_empty_lines(lines: list[str]) -> list[str]:
128
169
  start = 0
129
170
  end = len(lines) - 1
@@ -247,6 +288,16 @@ class FileEditInput:
247
288
  ]
248
289
 
249
290
  for match, tolerances in matches_with_tolerances:
291
+ if any(
292
+ tolerance.error_name == REMOVE_INDENTATION
293
+ for tolerance in tolerances
294
+ ):
295
+ replace_by = fix_indentation(
296
+ self.file_lines[match.start : match.stop],
297
+ first_block[0],
298
+ replace_by,
299
+ )
300
+
250
301
  file_edit_input = FileEditInput(
251
302
  self.file_lines,
252
303
  match.stop,
@@ -165,7 +165,7 @@ async def main() -> None:
165
165
  version = str(importlib.metadata.version("wcgw"))
166
166
  home_dir = os.path.expanduser("~")
167
167
  with BashState(
168
- Console(), home_dir, None, None, None, None, False, None
168
+ Console(), home_dir, None, None, None, None, True, None
169
169
  ) as BASH_STATE:
170
170
  BASH_STATE.console.log("wcgw version: " + version)
171
171
  # Run the server using stdin/stdout streams
@@ -206,6 +206,8 @@ Initialized in directory (also cwd): {context.bash_state.cwd}
206
206
  {memory}
207
207
  """
208
208
 
209
+ global INITIALIZED
210
+ INITIALIZED = True
209
211
  return output, context
210
212
 
211
213
 
@@ -257,6 +259,8 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
257
259
  list(context.bash_state.whitelist_for_overwrite),
258
260
  reset_wcgw.starting_directory,
259
261
  )
262
+ global INITIALIZED
263
+ INITIALIZED = True
260
264
  return "Reset successful" + get_status(context.bash_state)
261
265
 
262
266
 
@@ -595,7 +599,6 @@ def get_tool_output(
595
599
  context.console.print("Calling reset wcgw tool")
596
600
  output = reset_wcgw(context, arg), 0.0
597
601
 
598
- INITIALIZED = True
599
602
  elif isinstance(arg, Initialize):
600
603
  context.console.print("Calling initial info tool")
601
604
  output_, context = initialize(
@@ -608,7 +611,6 @@ def get_tool_output(
608
611
  )
609
612
  output = output_, 0.0
610
613
 
611
- INITIALIZED = True
612
614
  elif isinstance(arg, ContextSave):
613
615
  context.console.print("Calling task memory tool")
614
616
  relevant_files = []
@@ -25,7 +25,7 @@ def register_client(server_url: str, client_uuid: str = "") -> None:
25
25
  # Create the WebSocket connection and context
26
26
  the_console = rich.console.Console(style="magenta", highlight=False, markup=False)
27
27
  with BashState(
28
- the_console, os.getcwd(), None, None, None, None, False, None
28
+ the_console, os.getcwd(), None, None, None, None, True, None
29
29
  ) as bash_state:
30
30
  context = Context(bash_state=bash_state, console=the_console)
31
31
 
@@ -214,10 +214,9 @@ def loop(
214
214
  )
215
215
 
216
216
  with BashState(
217
- system_console, os.getcwd(), None, None, None, None, False, None
217
+ system_console, os.getcwd(), None, None, None, None, True, None
218
218
  ) as bash_state:
219
219
  context = Context(bash_state, system_console)
220
-
221
220
  system, context = initialize(
222
221
  context,
223
222
  os.getcwd(),
@@ -178,7 +178,7 @@ def loop(
178
178
  )
179
179
 
180
180
  with BashState(
181
- system_console, os.getcwd(), None, None, None, None, False, None
181
+ system_console, os.getcwd(), None, None, None, None, True, None
182
182
  ) as bash_state:
183
183
  context = Context(bash_state, system_console)
184
184
  system, context = initialize(