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.
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/PKG-INFO +1 -1
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/pyproject.toml +1 -1
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/bash_state/bash_state.py +137 -46
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/file_ops/diff_edit.py +52 -1
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/mcp_server/server.py +1 -1
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/tools.py +4 -2
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/relay/client.py +1 -1
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/anthropic_client.py +1 -2
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/openai_client.py +1 -1
- wcgw-3.0.1rc2/tests/test_edit.py +450 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/uv.lock +1 -1
- wcgw-3.0.1rc1/tests/test_edit.py +0 -269
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.github/workflows/python-publish.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.github/workflows/python-tests.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.github/workflows/python-types.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.gitignore +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.gitmodules +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.python-version +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/.vscode/settings.json +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/Dockerfile +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/LICENSE +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/README.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/gpt_action_json_schema.json +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/gpt_instructions.txt +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/openai.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.git +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.gitignore +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/.python-version +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/LICENSE +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/README.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/RELEASE.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/SECURITY.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/README.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/pyproject.toml +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/mcp_wcgw_fork/uv.lock +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/common.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/diff-instructions.txt +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/encoder/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/file_ops/search_replace.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/mcp_server/Readme.md +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/mcp_server/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/memory.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/modes.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/display_tree.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/path_prob.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/repo_ops/repo_context.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/client/tool_prompts.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/py.typed +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/relay/serve.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/relay/static/privacy.txt +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw/types_.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/__init__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/__main__.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/cli.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/src/wcgw_cli/openai_utils.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/claude-ss.jpg +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/computer-use.jpg +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/example.jpg +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/rocket-icon.png +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/static/ss1.png +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/tests/test_mcp_server.py +0 -0
- {wcgw-3.0.1rc1 → wcgw-3.0.1rc2}/tests/test_tools.py +0 -0
|
@@ -6,7 +6,15 @@ import threading
|
|
|
6
6
|
import time
|
|
7
7
|
import traceback
|
|
8
8
|
from dataclasses import dataclass
|
|
9
|
-
from typing import
|
|
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 -
|
|
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.
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
311
|
+
return shell.expect(pattern, timeout)
|
|
251
312
|
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
-
|
|
264
|
-
|
|
326
|
+
@requires_shell
|
|
327
|
+
def linesep(self, shell: "pexpect.spawn[str]") -> Any:
|
|
328
|
+
return shell.linesep
|
|
265
329
|
|
|
266
|
-
|
|
267
|
-
|
|
330
|
+
@requires_shell
|
|
331
|
+
def sendintr(self, shell: "pexpect.spawn[str]") -> None:
|
|
332
|
+
shell.sendintr()
|
|
268
333
|
|
|
269
334
|
@property
|
|
270
|
-
|
|
271
|
-
|
|
335
|
+
@requires_shell
|
|
336
|
+
def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
|
|
337
|
+
return shell.before
|
|
272
338
|
|
|
273
|
-
|
|
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 =
|
|
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.
|
|
304
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
460
|
+
over_screen=self._use_screen,
|
|
381
461
|
)
|
|
382
|
-
self.over_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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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.
|
|
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=
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|