wcgw 3.0.1rc1__py3-none-any.whl → 3.0.1rc3__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.
- wcgw/client/bash_state/bash_state.py +181 -82
- wcgw/client/file_ops/diff_edit.py +52 -1
- wcgw/client/mcp_server/server.py +10 -18
- wcgw/client/tools.py +23 -3
- wcgw/relay/client.py +1 -1
- {wcgw-3.0.1rc1.dist-info → wcgw-3.0.1rc3.dist-info}/METADATA +1 -1
- {wcgw-3.0.1rc1.dist-info → wcgw-3.0.1rc3.dist-info}/RECORD +12 -12
- wcgw_cli/anthropic_client.py +5 -6
- wcgw_cli/openai_client.py +1 -1
- {wcgw-3.0.1rc1.dist-info → wcgw-3.0.1rc3.dist-info}/WHEEL +0 -0
- {wcgw-3.0.1rc1.dist-info → wcgw-3.0.1rc3.dist-info}/entry_points.txt +0 -0
- {wcgw-3.0.1rc1.dist-info → wcgw-3.0.1rc3.dist-info}/licenses/LICENSE +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
|
|
@@ -24,7 +32,8 @@ from ...types_ import (
|
|
|
24
32
|
from ..encoder import EncoderDecoder
|
|
25
33
|
from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
|
|
26
34
|
|
|
27
|
-
PROMPT_CONST = "
|
|
35
|
+
PROMPT_CONST = "wcgw→" + " "
|
|
36
|
+
PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
|
|
28
37
|
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
29
38
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
30
39
|
|
|
@@ -63,7 +72,7 @@ def get_tmpdir() -> str:
|
|
|
63
72
|
timeout=CONFIG.timeout,
|
|
64
73
|
).strip()
|
|
65
74
|
return result
|
|
66
|
-
except subprocess.CalledProcessError:
|
|
75
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
67
76
|
return "//tmp"
|
|
68
77
|
except Exception:
|
|
69
78
|
return ""
|
|
@@ -71,9 +80,7 @@ def get_tmpdir() -> str:
|
|
|
71
80
|
|
|
72
81
|
def check_if_screen_command_available() -> bool:
|
|
73
82
|
try:
|
|
74
|
-
subprocess.run(
|
|
75
|
-
["screen", "-v"], capture_output=True, check=True, timeout=CONFIG.timeout
|
|
76
|
-
)
|
|
83
|
+
subprocess.run(["screen", "-v"], capture_output=True, check=True, timeout=0.2)
|
|
77
84
|
return True
|
|
78
85
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
79
86
|
return False
|
|
@@ -92,12 +99,14 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
92
99
|
capture_output=True,
|
|
93
100
|
text=True,
|
|
94
101
|
check=True,
|
|
95
|
-
timeout=
|
|
102
|
+
timeout=0.2,
|
|
96
103
|
)
|
|
97
104
|
output = result.stdout
|
|
98
105
|
except subprocess.CalledProcessError as e:
|
|
99
106
|
# When no screens exist, screen may return a non-zero exit code.
|
|
100
107
|
output = (e.stdout or "") + (e.stderr or "")
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
return
|
|
101
110
|
|
|
102
111
|
sessions_to_kill = []
|
|
103
112
|
|
|
@@ -121,7 +130,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
121
130
|
check=True,
|
|
122
131
|
timeout=CONFIG.timeout,
|
|
123
132
|
)
|
|
124
|
-
except subprocess.CalledProcessError:
|
|
133
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
125
134
|
console.log(f"Failed to kill screen session: {session}")
|
|
126
135
|
|
|
127
136
|
|
|
@@ -136,23 +145,21 @@ def start_shell(
|
|
|
136
145
|
**os.environ,
|
|
137
146
|
"PS1": PROMPT_CONST,
|
|
138
147
|
"TMPDIR": get_tmpdir(),
|
|
139
|
-
"TERM": "
|
|
148
|
+
"TERM": "xterm-256color",
|
|
140
149
|
}
|
|
141
150
|
try:
|
|
142
151
|
shell = pexpect.spawn(
|
|
143
152
|
cmd,
|
|
144
153
|
env=overrideenv, # type: ignore[arg-type]
|
|
145
|
-
echo=
|
|
154
|
+
echo=True,
|
|
146
155
|
encoding="utf-8",
|
|
147
156
|
timeout=CONFIG.timeout,
|
|
148
157
|
cwd=initial_dir,
|
|
149
158
|
codec_errors="backslashreplace",
|
|
150
159
|
dimensions=(500, 160),
|
|
151
160
|
)
|
|
152
|
-
shell.sendline(
|
|
153
|
-
|
|
154
|
-
) # Unset prompt command to avoid interfering
|
|
155
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
161
|
+
shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
|
|
162
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
156
163
|
except Exception as e:
|
|
157
164
|
console.print(traceback.format_exc())
|
|
158
165
|
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
@@ -160,13 +167,13 @@ def start_shell(
|
|
|
160
167
|
shell = pexpect.spawn(
|
|
161
168
|
"/bin/bash --noprofile --norc",
|
|
162
169
|
env=overrideenv, # type: ignore[arg-type]
|
|
163
|
-
echo=
|
|
170
|
+
echo=True,
|
|
164
171
|
encoding="utf-8",
|
|
165
172
|
timeout=CONFIG.timeout,
|
|
166
173
|
codec_errors="backslashreplace",
|
|
167
174
|
)
|
|
168
|
-
shell.sendline(
|
|
169
|
-
shell.expect(PROMPT_CONST, timeout=
|
|
175
|
+
shell.sendline(PROMPT_STATEMENT)
|
|
176
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
170
177
|
|
|
171
178
|
shellid = "wcgw." + time.strftime("%H%M%S")
|
|
172
179
|
if over_screen:
|
|
@@ -174,22 +181,11 @@ def start_shell(
|
|
|
174
181
|
raise ValueError("Screen command not available")
|
|
175
182
|
# shellid is just hour, minute, second number
|
|
176
183
|
shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
|
|
177
|
-
shell.expect(PROMPT_CONST, timeout=
|
|
184
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
178
185
|
|
|
179
|
-
shell.sendline(f"screen -q -
|
|
186
|
+
shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
|
|
180
187
|
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
181
188
|
|
|
182
|
-
console.log(f"Entering screen session, name: {shellid}")
|
|
183
|
-
|
|
184
|
-
shell.sendline("stty -icanon -echo")
|
|
185
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
186
|
-
|
|
187
|
-
shell.sendline("set +o pipefail")
|
|
188
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
189
|
-
|
|
190
|
-
shell.sendline("export GIT_PAGER=cat PAGER=cat")
|
|
191
|
-
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
192
|
-
|
|
193
189
|
return shell, shellid
|
|
194
190
|
|
|
195
191
|
|
|
@@ -217,7 +213,39 @@ def render_terminal_output(text: str) -> list[str]:
|
|
|
217
213
|
return lines
|
|
218
214
|
|
|
219
215
|
|
|
216
|
+
P = ParamSpec("P")
|
|
217
|
+
R = TypeVar("R")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def requires_shell(
|
|
221
|
+
func: Callable[Concatenate["BashState", "pexpect.spawn[str]", P], R],
|
|
222
|
+
) -> Callable[Concatenate["BashState", P], R]:
|
|
223
|
+
def wrapper(self: "BashState", /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
224
|
+
if not self._shell_loading.is_set():
|
|
225
|
+
if not self._shell_loading.wait(
|
|
226
|
+
timeout=CONFIG.timeout * 2
|
|
227
|
+
): # Twice in worst case if screen fails
|
|
228
|
+
raise RuntimeError("Shell initialization timeout")
|
|
229
|
+
|
|
230
|
+
if self._shell_error:
|
|
231
|
+
raise RuntimeError(f"Shell failed to initialize: {self._shell_error}.")
|
|
232
|
+
|
|
233
|
+
if not self._shell:
|
|
234
|
+
raise RuntimeError("Shell not initialized")
|
|
235
|
+
|
|
236
|
+
return func(self, self._shell, *args, **kwargs)
|
|
237
|
+
|
|
238
|
+
return wrapper
|
|
239
|
+
|
|
240
|
+
|
|
220
241
|
class BashState:
|
|
242
|
+
_shell: Optional["pexpect.spawn[str]"]
|
|
243
|
+
_shell_id: Optional[str]
|
|
244
|
+
_shell_lock: threading.Lock
|
|
245
|
+
_shell_loading: threading.Event
|
|
246
|
+
_shell_error: Optional[Exception]
|
|
247
|
+
_use_screen: bool
|
|
248
|
+
|
|
221
249
|
def __init__(
|
|
222
250
|
self,
|
|
223
251
|
console: Console,
|
|
@@ -240,35 +268,65 @@ class BashState:
|
|
|
240
268
|
)
|
|
241
269
|
self._mode = mode or Modes.wcgw
|
|
242
270
|
self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
|
|
243
|
-
self._prompt = PROMPT_CONST
|
|
244
271
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
245
272
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
246
|
-
self.
|
|
247
|
-
|
|
248
|
-
|
|
273
|
+
self._shell = None
|
|
274
|
+
self._shell_id = None
|
|
275
|
+
self._shell_lock = threading.Lock()
|
|
276
|
+
self._shell_loading = threading.Event()
|
|
277
|
+
self._shell_error = None
|
|
278
|
+
self._use_screen = use_screen
|
|
279
|
+
self._start_shell_loading()
|
|
280
|
+
|
|
281
|
+
def _start_shell_loading(self) -> None:
|
|
282
|
+
def load_shell() -> None:
|
|
283
|
+
try:
|
|
284
|
+
with self._shell_lock:
|
|
285
|
+
if self._shell is not None:
|
|
286
|
+
return
|
|
287
|
+
self._init_shell()
|
|
288
|
+
self.run_bg_expect_thread()
|
|
289
|
+
except Exception as e:
|
|
290
|
+
self._shell_error = e
|
|
291
|
+
finally:
|
|
292
|
+
self._shell_loading.set()
|
|
293
|
+
|
|
294
|
+
threading.Thread(target=load_shell).start()
|
|
295
|
+
|
|
296
|
+
@requires_shell
|
|
297
|
+
def expect(
|
|
298
|
+
self, shell: "pexpect.spawn[str]", pattern: Any, timeout: Optional[float] = -1
|
|
299
|
+
) -> int:
|
|
249
300
|
self.close_bg_expect_thread()
|
|
250
|
-
|
|
301
|
+
output = shell.expect(pattern, timeout)
|
|
302
|
+
return output
|
|
251
303
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
self.
|
|
304
|
+
@requires_shell
|
|
305
|
+
def send(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
|
|
306
|
+
self.close_bg_expect_thread()
|
|
307
|
+
output = shell.send(s)
|
|
255
308
|
return output
|
|
256
309
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
self.
|
|
310
|
+
@requires_shell
|
|
311
|
+
def sendline(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
|
|
312
|
+
self.close_bg_expect_thread()
|
|
313
|
+
output = shell.sendline(s)
|
|
260
314
|
return output
|
|
261
315
|
|
|
262
316
|
@property
|
|
263
|
-
|
|
264
|
-
|
|
317
|
+
@requires_shell
|
|
318
|
+
def linesep(self, shell: "pexpect.spawn[str]") -> Any:
|
|
319
|
+
return shell.linesep
|
|
265
320
|
|
|
266
|
-
|
|
267
|
-
|
|
321
|
+
@requires_shell
|
|
322
|
+
def sendintr(self, shell: "pexpect.spawn[str]") -> None:
|
|
323
|
+
self.close_bg_expect_thread()
|
|
324
|
+
shell.sendintr()
|
|
268
325
|
|
|
269
326
|
@property
|
|
270
|
-
|
|
271
|
-
|
|
327
|
+
@requires_shell
|
|
328
|
+
def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
|
|
329
|
+
return shell.before
|
|
272
330
|
|
|
273
331
|
def run_bg_expect_thread(self) -> None:
|
|
274
332
|
"""
|
|
@@ -279,11 +337,16 @@ class BashState:
|
|
|
279
337
|
while True:
|
|
280
338
|
if self._bg_expect_thread_stop_event.is_set():
|
|
281
339
|
break
|
|
340
|
+
if self._shell is None:
|
|
341
|
+
time.sleep(0.1)
|
|
342
|
+
continue
|
|
282
343
|
output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
|
|
283
344
|
if output == 0:
|
|
284
345
|
break
|
|
285
346
|
|
|
286
|
-
if self._bg_expect_thread:
|
|
347
|
+
if self._bg_expect_thread and self._bg_expect_thread.is_alive():
|
|
348
|
+
if not self._bg_expect_thread_stop_event.is_set():
|
|
349
|
+
return
|
|
287
350
|
self.close_bg_expect_thread()
|
|
288
351
|
|
|
289
352
|
self._bg_expect_thread = threading.Thread(
|
|
@@ -300,8 +363,13 @@ class BashState:
|
|
|
300
363
|
|
|
301
364
|
def cleanup(self) -> None:
|
|
302
365
|
self.close_bg_expect_thread()
|
|
303
|
-
self.
|
|
304
|
-
|
|
366
|
+
with self._shell_lock:
|
|
367
|
+
if self._shell:
|
|
368
|
+
self._shell.close(True)
|
|
369
|
+
if self._shell_id:
|
|
370
|
+
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
371
|
+
self._shell = None
|
|
372
|
+
self._shell_id = None
|
|
305
373
|
|
|
306
374
|
def __enter__(self) -> "BashState":
|
|
307
375
|
return self
|
|
@@ -325,26 +393,26 @@ class BashState:
|
|
|
325
393
|
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
326
394
|
return self._write_if_empty_mode
|
|
327
395
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
396
|
+
@requires_shell
|
|
397
|
+
def ensure_env_and_bg_jobs(self, _: "pexpect.spawn[str]") -> Optional[int]:
|
|
398
|
+
return self._ensure_env_and_bg_jobs()
|
|
399
|
+
|
|
400
|
+
def _ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
401
|
+
# Do not add @requires_shell decorator here, as it will cause deadlock
|
|
402
|
+
self.close_bg_expect_thread()
|
|
403
|
+
assert self._shell is not None, "Bad state, shell is not initialized"
|
|
331
404
|
quick_timeout = 0.2 if not self.over_screen else 1
|
|
332
405
|
# First reset the prompt in case venv was sourced or other reasons.
|
|
333
|
-
self.sendline(
|
|
334
|
-
self.expect(
|
|
406
|
+
self._shell.sendline(PROMPT_STATEMENT)
|
|
407
|
+
self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
335
408
|
# Reset echo also if it was enabled
|
|
336
|
-
|
|
337
|
-
self.
|
|
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")
|
|
409
|
+
command = "jobs | wc -l"
|
|
410
|
+
self._shell.sendline(command)
|
|
343
411
|
before = ""
|
|
344
412
|
counts = 0
|
|
345
413
|
while not _is_int(before): # Consume all previous output
|
|
346
414
|
try:
|
|
347
|
-
self.expect(
|
|
415
|
+
self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
348
416
|
except pexpect.TIMEOUT:
|
|
349
417
|
self.console.print(f"Couldn't get exit code, before: {before}")
|
|
350
418
|
raise
|
|
@@ -354,20 +422,18 @@ class BashState:
|
|
|
354
422
|
before_val = str(before_val)
|
|
355
423
|
assert isinstance(before_val, str)
|
|
356
424
|
before_lines = render_terminal_output(before_val)
|
|
357
|
-
before = "\n".join(before_lines).strip()
|
|
425
|
+
before = "\n".join(before_lines).replace(command, "").strip()
|
|
358
426
|
counts += 1
|
|
359
427
|
if counts > 100:
|
|
360
428
|
raise ValueError(
|
|
361
429
|
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
362
430
|
)
|
|
363
|
-
|
|
364
431
|
try:
|
|
365
432
|
return int(before)
|
|
366
433
|
except ValueError:
|
|
367
434
|
raise ValueError(f"Malformed output: {before}")
|
|
368
435
|
|
|
369
|
-
def _init_shell(self
|
|
370
|
-
self._prompt = PROMPT_CONST
|
|
436
|
+
def _init_shell(self) -> None:
|
|
371
437
|
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
372
438
|
self._is_in_docker: Optional[str] = ""
|
|
373
439
|
# Ensure self._cwd exists
|
|
@@ -377,9 +443,9 @@ class BashState:
|
|
|
377
443
|
self._bash_command_mode.bash_mode == "restricted_mode",
|
|
378
444
|
self._cwd,
|
|
379
445
|
self.console,
|
|
380
|
-
over_screen=
|
|
446
|
+
over_screen=self._use_screen,
|
|
381
447
|
)
|
|
382
|
-
self.over_screen =
|
|
448
|
+
self.over_screen = self._use_screen
|
|
383
449
|
except Exception as e:
|
|
384
450
|
if not isinstance(e, ValueError):
|
|
385
451
|
self.console.log(traceback.format_exc())
|
|
@@ -394,15 +460,15 @@ class BashState:
|
|
|
394
460
|
self.over_screen = False
|
|
395
461
|
|
|
396
462
|
self._pending_output = ""
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
463
|
+
try:
|
|
464
|
+
self._ensure_env_and_bg_jobs()
|
|
465
|
+
except ValueError as e:
|
|
466
|
+
self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
|
|
400
467
|
|
|
401
468
|
def set_pending(self, last_pending_output: str) -> None:
|
|
402
469
|
if not isinstance(self._state, datetime.datetime):
|
|
403
470
|
self._state = datetime.datetime.now()
|
|
404
471
|
self._pending_output = last_pending_output
|
|
405
|
-
self.run_bg_expect_thread()
|
|
406
472
|
|
|
407
473
|
def set_repl(self) -> None:
|
|
408
474
|
self._state = "repl"
|
|
@@ -427,12 +493,13 @@ class BashState:
|
|
|
427
493
|
|
|
428
494
|
@property
|
|
429
495
|
def prompt(self) -> str:
|
|
430
|
-
return
|
|
496
|
+
return PROMPT_CONST
|
|
431
497
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
498
|
+
@requires_shell
|
|
499
|
+
def update_cwd(self, shell: "pexpect.spawn[str]") -> str:
|
|
500
|
+
shell.sendline("pwd")
|
|
501
|
+
shell.expect(PROMPT_CONST, timeout=0.2)
|
|
502
|
+
before_val = shell.before
|
|
436
503
|
if not isinstance(before_val, str):
|
|
437
504
|
before_val = str(before_val)
|
|
438
505
|
before_lines = render_terminal_output(before_val)
|
|
@@ -442,7 +509,9 @@ class BashState:
|
|
|
442
509
|
|
|
443
510
|
def reset_shell(self) -> None:
|
|
444
511
|
self.cleanup()
|
|
445
|
-
self.
|
|
512
|
+
self._shell_loading.clear()
|
|
513
|
+
self._shell_error = None
|
|
514
|
+
self._start_shell_loading()
|
|
446
515
|
|
|
447
516
|
def serialize(self) -> dict[str, Any]:
|
|
448
517
|
"""Serialize BashState to a dictionary for saving"""
|
|
@@ -476,6 +545,16 @@ class BashState:
|
|
|
476
545
|
cwd: str,
|
|
477
546
|
) -> None:
|
|
478
547
|
"""Create a new BashState instance from a serialized state dictionary"""
|
|
548
|
+
if (
|
|
549
|
+
self._bash_command_mode == bash_command_mode
|
|
550
|
+
and ((self._cwd == cwd) or not cwd)
|
|
551
|
+
and (self._file_edit_mode == file_edit_mode)
|
|
552
|
+
and (self._write_if_empty_mode == write_if_empty_mode)
|
|
553
|
+
and (self._mode == mode)
|
|
554
|
+
and (self._whitelist_for_overwrite == set(whitelist_for_overwrite))
|
|
555
|
+
):
|
|
556
|
+
# No need to reset shell if the state is the same
|
|
557
|
+
return
|
|
479
558
|
self._bash_command_mode = bash_command_mode
|
|
480
559
|
self._cwd = cwd or self._cwd
|
|
481
560
|
self._file_edit_mode = file_edit_mode
|
|
@@ -546,9 +625,8 @@ def _incremental_text(text: str, last_pending_output: str) -> str:
|
|
|
546
625
|
# text = render_terminal_output(text[-100_000:])
|
|
547
626
|
text = text[-100_000:]
|
|
548
627
|
|
|
549
|
-
|
|
550
|
-
last_pending_output_rendered = "\n".join(
|
|
551
|
-
last_rendered_lines = last_pending_output_rendered.split("\n")
|
|
628
|
+
last_rendered_lines = render_terminal_output(last_pending_output)
|
|
629
|
+
last_pending_output_rendered = "\n".join(last_rendered_lines)
|
|
552
630
|
if not last_rendered_lines:
|
|
553
631
|
return rstrip(render_terminal_output(text))
|
|
554
632
|
|
|
@@ -599,6 +677,27 @@ def execute_bash(
|
|
|
599
677
|
bash_arg: BashCommand,
|
|
600
678
|
max_tokens: Optional[int],
|
|
601
679
|
timeout_s: Optional[float],
|
|
680
|
+
) -> tuple[str, float]:
|
|
681
|
+
try:
|
|
682
|
+
output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
|
|
683
|
+
|
|
684
|
+
# Remove echo if it's a command
|
|
685
|
+
if isinstance(bash_arg.action, Command):
|
|
686
|
+
command = bash_arg.action.command.strip()
|
|
687
|
+
if output.startswith(command):
|
|
688
|
+
output = output[len(command) :]
|
|
689
|
+
|
|
690
|
+
finally:
|
|
691
|
+
bash_state.run_bg_expect_thread()
|
|
692
|
+
return output, cost
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def _execute_bash(
|
|
696
|
+
bash_state: BashState,
|
|
697
|
+
enc: EncoderDecoder[int],
|
|
698
|
+
bash_arg: BashCommand,
|
|
699
|
+
max_tokens: Optional[int],
|
|
700
|
+
timeout_s: Optional[float],
|
|
602
701
|
) -> tuple[str, float]:
|
|
603
702
|
try:
|
|
604
703
|
is_interrupt = False
|
|
@@ -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,
|
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import importlib
|
|
2
|
-
import json
|
|
3
2
|
import logging
|
|
4
3
|
import os
|
|
5
4
|
from typing import Any
|
|
@@ -9,7 +8,7 @@ import mcp_wcgw.types as types
|
|
|
9
8
|
from mcp_wcgw.server import NotificationOptions, Server
|
|
10
9
|
from mcp_wcgw.server.models import InitializationOptions
|
|
11
10
|
from mcp_wcgw.types import Tool as ToolParam
|
|
12
|
-
from pydantic import AnyUrl
|
|
11
|
+
from pydantic import AnyUrl
|
|
13
12
|
|
|
14
13
|
from wcgw.client.modes import KTS
|
|
15
14
|
from wcgw.client.tool_prompts import TOOL_PROMPTS
|
|
@@ -18,7 +17,13 @@ from ...types_ import (
|
|
|
18
17
|
Initialize,
|
|
19
18
|
)
|
|
20
19
|
from ..bash_state.bash_state import CONFIG, BashState
|
|
21
|
-
from ..tools import
|
|
20
|
+
from ..tools import (
|
|
21
|
+
Context,
|
|
22
|
+
default_enc,
|
|
23
|
+
get_tool_output,
|
|
24
|
+
parse_tool_by_name,
|
|
25
|
+
which_tool_name,
|
|
26
|
+
)
|
|
22
27
|
|
|
23
28
|
server = Server("wcgw")
|
|
24
29
|
|
|
@@ -104,20 +109,7 @@ async def handle_call_tool(
|
|
|
104
109
|
raise ValueError("Missing arguments")
|
|
105
110
|
|
|
106
111
|
tool_type = which_tool_name(name)
|
|
107
|
-
|
|
108
|
-
try:
|
|
109
|
-
tool_call = tool_type(**arguments)
|
|
110
|
-
except ValidationError:
|
|
111
|
-
|
|
112
|
-
def try_json(x: str) -> Any:
|
|
113
|
-
if not isinstance(x, str):
|
|
114
|
-
return x
|
|
115
|
-
try:
|
|
116
|
-
return json.loads(x)
|
|
117
|
-
except json.JSONDecodeError:
|
|
118
|
-
return x
|
|
119
|
-
|
|
120
|
-
tool_call = tool_type(**{k: try_json(v) for k, v in arguments.items()})
|
|
112
|
+
tool_call = parse_tool_by_name(name, arguments)
|
|
121
113
|
|
|
122
114
|
try:
|
|
123
115
|
assert BASH_STATE
|
|
@@ -165,7 +157,7 @@ async def main() -> None:
|
|
|
165
157
|
version = str(importlib.metadata.version("wcgw"))
|
|
166
158
|
home_dir = os.path.expanduser("~")
|
|
167
159
|
with BashState(
|
|
168
|
-
Console(), home_dir, None, None, None, None,
|
|
160
|
+
Console(), home_dir, None, None, None, None, True, None
|
|
169
161
|
) as BASH_STATE:
|
|
170
162
|
BASH_STATE.console.log("wcgw version: " + version)
|
|
171
163
|
# Run the server using stdin/stdout streams
|
wcgw/client/tools.py
CHANGED
|
@@ -10,6 +10,7 @@ from os.path import expanduser
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from tempfile import NamedTemporaryFile
|
|
12
12
|
from typing import (
|
|
13
|
+
Any,
|
|
13
14
|
Callable,
|
|
14
15
|
Literal,
|
|
15
16
|
Optional,
|
|
@@ -21,7 +22,7 @@ from typing import (
|
|
|
21
22
|
from openai.types.chat import (
|
|
22
23
|
ChatCompletionMessageParam,
|
|
23
24
|
)
|
|
24
|
-
from pydantic import BaseModel, TypeAdapter
|
|
25
|
+
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
25
26
|
from syntax_checker import check_syntax
|
|
26
27
|
|
|
27
28
|
from wcgw.client.bash_state.bash_state import get_status
|
|
@@ -206,6 +207,8 @@ Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
|
206
207
|
{memory}
|
|
207
208
|
"""
|
|
208
209
|
|
|
210
|
+
global INITIALIZED
|
|
211
|
+
INITIALIZED = True
|
|
209
212
|
return output, context
|
|
210
213
|
|
|
211
214
|
|
|
@@ -257,6 +260,8 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
|
|
|
257
260
|
list(context.bash_state.whitelist_for_overwrite),
|
|
258
261
|
reset_wcgw.starting_directory,
|
|
259
262
|
)
|
|
263
|
+
global INITIALIZED
|
|
264
|
+
INITIALIZED = True
|
|
260
265
|
return "Reset successful" + get_status(context.bash_state)
|
|
261
266
|
|
|
262
267
|
|
|
@@ -545,6 +550,23 @@ def which_tool_name(name: str) -> Type[TOOLS]:
|
|
|
545
550
|
raise ValueError(f"Unknown tool name: {name}")
|
|
546
551
|
|
|
547
552
|
|
|
553
|
+
def parse_tool_by_name(name: str, arguments: dict[str, Any]) -> TOOLS:
|
|
554
|
+
tool_type = which_tool_name(name)
|
|
555
|
+
try:
|
|
556
|
+
return tool_type(**arguments)
|
|
557
|
+
except ValidationError:
|
|
558
|
+
|
|
559
|
+
def try_json(x: str) -> Any:
|
|
560
|
+
if not isinstance(x, str):
|
|
561
|
+
return x
|
|
562
|
+
try:
|
|
563
|
+
return json.loads(x)
|
|
564
|
+
except json.JSONDecodeError:
|
|
565
|
+
return x
|
|
566
|
+
|
|
567
|
+
return tool_type(**{k: try_json(v) for k, v in arguments.items()})
|
|
568
|
+
|
|
569
|
+
|
|
548
570
|
TOOL_CALLS: list[TOOLS] = []
|
|
549
571
|
|
|
550
572
|
|
|
@@ -595,7 +617,6 @@ def get_tool_output(
|
|
|
595
617
|
context.console.print("Calling reset wcgw tool")
|
|
596
618
|
output = reset_wcgw(context, arg), 0.0
|
|
597
619
|
|
|
598
|
-
INITIALIZED = True
|
|
599
620
|
elif isinstance(arg, Initialize):
|
|
600
621
|
context.console.print("Calling initial info tool")
|
|
601
622
|
output_, context = initialize(
|
|
@@ -608,7 +629,6 @@ def get_tool_output(
|
|
|
608
629
|
)
|
|
609
630
|
output = output_, 0.0
|
|
610
631
|
|
|
611
|
-
INITIALIZED = True
|
|
612
632
|
elif isinstance(arg, ContextSave):
|
|
613
633
|
context.console.print("Calling task memory tool")
|
|
614
634
|
relevant_files = []
|
wcgw/relay/client.py
CHANGED
|
@@ -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
|
|
|
@@ -7,27 +7,27 @@ wcgw/client/diff-instructions.txt,sha256=tmJ9Fu9XdO_72lYXQQNY9RZyx91bjxrXJf9d_KB
|
|
|
7
7
|
wcgw/client/memory.py,sha256=8LdYsOhvCOoC1kfvDr85kNy07WnhPMvE6B2FRM2w85Y,2902
|
|
8
8
|
wcgw/client/modes.py,sha256=dBkiMNQevTFNkhplrDsDuSIyeU-wLmAIfAa9Dqowvk8,10387
|
|
9
9
|
wcgw/client/tool_prompts.py,sha256=j82aYnlr1pcinsOYitSrlyYz4-9K3KcqnAfGqot21NQ,4158
|
|
10
|
-
wcgw/client/tools.py,sha256=
|
|
11
|
-
wcgw/client/bash_state/bash_state.py,sha256=
|
|
10
|
+
wcgw/client/tools.py,sha256=caj10NHeK5tZ5Gi0L_oXzPcKBycgXeNaLEbXAzYTpqo,23170
|
|
11
|
+
wcgw/client/bash_state/bash_state.py,sha256=2cxzYN6x-vEJ4oasrCgoRyVQKpARKONcaNDLfLPDTG8,29476
|
|
12
12
|
wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
|
|
13
|
-
wcgw/client/file_ops/diff_edit.py,sha256=
|
|
13
|
+
wcgw/client/file_ops/diff_edit.py,sha256=sIwXSSkWYff_Dp3oHfefqtSuFqLrxbhKvzkCoLuLqDE,18679
|
|
14
14
|
wcgw/client/file_ops/search_replace.py,sha256=Napa7IWaYPGMNdttunKyRDkb90elZE7r23B_o_htRxo,5585
|
|
15
15
|
wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
|
|
16
16
|
wcgw/client/mcp_server/__init__.py,sha256=mm7xhBIPwJpRT3u-Qsj4cKVMpVyucJoKRlbMP_gRRB0,343
|
|
17
|
-
wcgw/client/mcp_server/server.py,sha256=
|
|
17
|
+
wcgw/client/mcp_server/server.py,sha256=ayK6qbzCveoQW7RO80m10cAIS3m-hvxzd15XhjiyxmE,5055
|
|
18
18
|
wcgw/client/repo_ops/display_tree.py,sha256=E5q9mrGBb57NyvudSmRIG-fj4FUqupbzjmARpX8X0XY,4166
|
|
19
19
|
wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
|
|
20
20
|
wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
|
|
21
21
|
wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5rxTjoNnCos,1569052
|
|
22
22
|
wcgw/client/repo_ops/repo_context.py,sha256=5NqRxBY0K-SBFXJ0Ybt7llzYOBD8pRkTpruMMJHWxv4,4336
|
|
23
|
-
wcgw/relay/client.py,sha256=
|
|
23
|
+
wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
|
|
24
24
|
wcgw/relay/serve.py,sha256=Ofq6PjW3zVVA2-9MVviGRiUESTD3sXb-482Q4RV13q8,8664
|
|
25
25
|
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
26
26
|
wcgw_cli/__init__.py,sha256=TNxXsTPgb52OhakIda9wTRh91cqoBqgQRx5TxjzQQFU,21
|
|
27
27
|
wcgw_cli/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
28
|
-
wcgw_cli/anthropic_client.py,sha256=
|
|
28
|
+
wcgw_cli/anthropic_client.py,sha256=2QLFBLbMeQuixF7Pz9j_hINHTG1CF9IYQZeri7zFuF0,18964
|
|
29
29
|
wcgw_cli/cli.py,sha256=-7FBe_lahKyUOhf65iurTA1M1gXXXAiT0OVKQVcZKKo,948
|
|
30
|
-
wcgw_cli/openai_client.py,sha256=
|
|
30
|
+
wcgw_cli/openai_client.py,sha256=oMFAaOkvXQtOY7choylVRJfaF2SnWvRc02ygQhlhVqY,15995
|
|
31
31
|
wcgw_cli/openai_utils.py,sha256=xGOb3W5ALrIozV7oszfGYztpj0FnXdD7jAxm5lEIVKY,2439
|
|
32
32
|
mcp_wcgw/__init__.py,sha256=fKCgOdN7cn7gR3YGFaGyV5Goe8A2sEyllLcsRkN0i-g,2601
|
|
33
33
|
mcp_wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -51,8 +51,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
51
51
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
52
52
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
53
53
|
mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
|
|
54
|
-
wcgw-3.0.
|
|
55
|
-
wcgw-3.0.
|
|
56
|
-
wcgw-3.0.
|
|
57
|
-
wcgw-3.0.
|
|
58
|
-
wcgw-3.0.
|
|
54
|
+
wcgw-3.0.1rc3.dist-info/METADATA,sha256=ymiydLYH56IAV_8FKq98BrGf-tt7Vwumtc8nUBvzs50,13005
|
|
55
|
+
wcgw-3.0.1rc3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
56
|
+
wcgw-3.0.1rc3.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
|
|
57
|
+
wcgw-3.0.1rc3.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
58
|
+
wcgw-3.0.1rc3.dist-info/RECORD,,
|
wcgw_cli/anthropic_client.py
CHANGED
|
@@ -35,7 +35,7 @@ from wcgw.client.tools import (
|
|
|
35
35
|
default_enc,
|
|
36
36
|
get_tool_output,
|
|
37
37
|
initialize,
|
|
38
|
-
|
|
38
|
+
parse_tool_by_name,
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
|
|
@@ -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(),
|
|
@@ -380,9 +379,9 @@ def loop(
|
|
|
380
379
|
tool_input = str(tc["input"])
|
|
381
380
|
tool_id = str(tc["id"])
|
|
382
381
|
|
|
383
|
-
tool_parsed =
|
|
384
|
-
tool_name
|
|
385
|
-
)
|
|
382
|
+
tool_parsed = parse_tool_by_name(
|
|
383
|
+
tool_name, json.loads(tool_input)
|
|
384
|
+
)
|
|
386
385
|
|
|
387
386
|
system_console.print(
|
|
388
387
|
f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
|
wcgw_cli/openai_client.py
CHANGED
|
@@ -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(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|