wcgw 5.4.5__py3-none-any.whl → 5.5.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.
- wcgw/client/bash_state/bash_state.py +323 -113
- wcgw/client/diff-instructions.txt +11 -9
- wcgw/client/mcp_server/__init__.py +4 -1
- wcgw/client/mcp_server/server.py +2 -2
- wcgw/client/modes.py +3 -2
- wcgw/client/tool_prompts.py +4 -2
- wcgw/client/tools.py +39 -10
- wcgw/types_.py +13 -0
- {wcgw-5.4.5.dist-info → wcgw-5.5.1.dist-info}/METADATA +27 -7
- {wcgw-5.4.5.dist-info → wcgw-5.5.1.dist-info}/RECORD +15 -15
- wcgw_cli/anthropic_client.py +1 -1
- wcgw_cli/openai_client.py +1 -1
- {wcgw-5.4.5.dist-info → wcgw-5.5.1.dist-info}/WHEEL +0 -0
- {wcgw-5.4.5.dist-info → wcgw-5.5.1.dist-info}/entry_points.txt +0 -0
- {wcgw-5.4.5.dist-info → wcgw-5.5.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,13 +3,15 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
5
|
import random
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
6
8
|
import subprocess
|
|
7
9
|
import tempfile
|
|
8
10
|
import threading
|
|
9
11
|
import time
|
|
10
12
|
import traceback
|
|
11
13
|
from dataclasses import dataclass
|
|
12
|
-
from hashlib import sha256
|
|
14
|
+
from hashlib import md5, sha256
|
|
13
15
|
from typing import (
|
|
14
16
|
Any,
|
|
15
17
|
Literal,
|
|
@@ -17,6 +19,7 @@ from typing import (
|
|
|
17
19
|
ParamSpec,
|
|
18
20
|
TypeVar,
|
|
19
21
|
)
|
|
22
|
+
from uuid import uuid4
|
|
20
23
|
|
|
21
24
|
import pexpect
|
|
22
25
|
import psutil
|
|
@@ -36,8 +39,9 @@ from ..encoder import EncoderDecoder
|
|
|
36
39
|
from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
|
|
37
40
|
from .parser.bash_statement_parser import BashStatementParser
|
|
38
41
|
|
|
39
|
-
PROMPT_CONST = "
|
|
40
|
-
|
|
42
|
+
PROMPT_CONST = re.compile(r"◉ ([^\n]*)──➤")
|
|
43
|
+
PROMPT_COMMAND = "printf '◉ '\"$(pwd)\"'──➤'' \r\\e[2K'"
|
|
44
|
+
PROMPT_STATEMENT = ""
|
|
41
45
|
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
42
46
|
os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
43
47
|
|
|
@@ -85,8 +89,23 @@ def get_tmpdir() -> str:
|
|
|
85
89
|
def check_if_screen_command_available() -> bool:
|
|
86
90
|
try:
|
|
87
91
|
subprocess.run(
|
|
88
|
-
["which", "screen"],
|
|
92
|
+
["which", "screen"],
|
|
93
|
+
capture_output=True,
|
|
94
|
+
check=True,
|
|
95
|
+
timeout=CONFIG.timeout,
|
|
89
96
|
)
|
|
97
|
+
|
|
98
|
+
# Check if screenrc exists, create it if it doesn't
|
|
99
|
+
home_dir = os.path.expanduser("~")
|
|
100
|
+
screenrc_path = os.path.join(home_dir, ".screenrc")
|
|
101
|
+
|
|
102
|
+
if not os.path.exists(screenrc_path):
|
|
103
|
+
screenrc_content = """defscrollback 10000
|
|
104
|
+
termcapinfo xterm* ti@:te@
|
|
105
|
+
"""
|
|
106
|
+
with open(screenrc_path, "w") as f:
|
|
107
|
+
f.write(screenrc_content)
|
|
108
|
+
|
|
90
109
|
return True
|
|
91
110
|
except (subprocess.CalledProcessError, FileNotFoundError, TimeoutError):
|
|
92
111
|
return False
|
|
@@ -215,7 +234,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
215
234
|
capture_output=True,
|
|
216
235
|
text=True,
|
|
217
236
|
check=True,
|
|
218
|
-
timeout=
|
|
237
|
+
timeout=CONFIG.timeout,
|
|
219
238
|
)
|
|
220
239
|
output = result.stdout
|
|
221
240
|
except subprocess.CalledProcessError as e:
|
|
@@ -252,18 +271,112 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
252
271
|
console.log(f"Failed to kill screen session: {session}\n{e}")
|
|
253
272
|
|
|
254
273
|
|
|
274
|
+
def get_rc_file_path(shell_path: str) -> Optional[str]:
|
|
275
|
+
"""
|
|
276
|
+
Get the rc file path for the given shell.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
shell_path: Path to the shell executable
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Path to the rc file or None if not supported
|
|
283
|
+
"""
|
|
284
|
+
shell_name = os.path.basename(shell_path)
|
|
285
|
+
home_dir = os.path.expanduser("~")
|
|
286
|
+
|
|
287
|
+
if shell_name == "zsh":
|
|
288
|
+
return os.path.join(home_dir, ".zshrc")
|
|
289
|
+
elif shell_name == "bash":
|
|
290
|
+
return os.path.join(home_dir, ".bashrc")
|
|
291
|
+
else:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def ensure_wcgw_block_in_rc_file(shell_path: str, console: Console) -> None:
|
|
296
|
+
"""
|
|
297
|
+
Ensure the WCGW environment block exists in the appropriate rc file.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
shell_path: Path to the shell executable
|
|
301
|
+
console: Console for logging
|
|
302
|
+
"""
|
|
303
|
+
rc_file_path = get_rc_file_path(shell_path)
|
|
304
|
+
if not rc_file_path:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
shell_name = os.path.basename(shell_path)
|
|
308
|
+
|
|
309
|
+
# Define the WCGW block with marker comments
|
|
310
|
+
marker_start = "# --WCGW_ENVIRONMENT_START--"
|
|
311
|
+
marker_end = "# --WCGW_ENVIRONMENT_END--"
|
|
312
|
+
|
|
313
|
+
if shell_name == "zsh":
|
|
314
|
+
wcgw_block = f"""{marker_start}
|
|
315
|
+
if [ -n "$IN_WCGW_ENVIRONMENT" ]; then
|
|
316
|
+
PROMPT_COMMAND='printf "◉ $(pwd)──➤ \\r\\e[2K"'
|
|
317
|
+
prmptcmdwcgw() {{ eval "$PROMPT_COMMAND" }}
|
|
318
|
+
add-zsh-hook -d precmd prmptcmdwcgw
|
|
319
|
+
precmd_functions+=prmptcmdwcgw
|
|
320
|
+
fi
|
|
321
|
+
{marker_end}
|
|
322
|
+
"""
|
|
323
|
+
elif shell_name == "bash":
|
|
324
|
+
wcgw_block = f"""{marker_start}
|
|
325
|
+
if [ -n "$IN_WCGW_ENVIRONMENT" ]; then
|
|
326
|
+
PROMPT_COMMAND='printf "◉ $(pwd)──➤ \\r\\e[2K"'
|
|
327
|
+
fi
|
|
328
|
+
{marker_end}
|
|
329
|
+
"""
|
|
330
|
+
else:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
# Check if rc file exists
|
|
334
|
+
if not os.path.exists(rc_file_path):
|
|
335
|
+
# Create the rc file with the WCGW block
|
|
336
|
+
try:
|
|
337
|
+
with open(rc_file_path, "w") as f:
|
|
338
|
+
f.write(wcgw_block)
|
|
339
|
+
console.log(f"Created {rc_file_path} with WCGW environment block")
|
|
340
|
+
except Exception as e:
|
|
341
|
+
console.log(f"Failed to create {rc_file_path}: {e}")
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
# Check if the block already exists
|
|
345
|
+
try:
|
|
346
|
+
with open(rc_file_path) as f:
|
|
347
|
+
content = f.read()
|
|
348
|
+
|
|
349
|
+
if marker_start in content:
|
|
350
|
+
# Block already exists
|
|
351
|
+
return
|
|
352
|
+
|
|
353
|
+
# Append the block to the file
|
|
354
|
+
with open(rc_file_path, "a") as f:
|
|
355
|
+
f.write("\n" + wcgw_block)
|
|
356
|
+
console.log(f"Added WCGW environment block to {rc_file_path}")
|
|
357
|
+
except Exception as e:
|
|
358
|
+
console.log(f"Failed to update {rc_file_path}: {e}")
|
|
359
|
+
|
|
360
|
+
|
|
255
361
|
def start_shell(
|
|
256
|
-
is_restricted_mode: bool,
|
|
362
|
+
is_restricted_mode: bool,
|
|
363
|
+
initial_dir: str,
|
|
364
|
+
console: Console,
|
|
365
|
+
over_screen: bool,
|
|
366
|
+
shell_path: str,
|
|
257
367
|
) -> tuple["pexpect.spawn[str]", str]:
|
|
258
|
-
cmd =
|
|
259
|
-
if is_restricted_mode:
|
|
368
|
+
cmd = shell_path
|
|
369
|
+
if is_restricted_mode and cmd.split("/")[-1] == "bash":
|
|
260
370
|
cmd += " -r"
|
|
261
371
|
|
|
262
372
|
overrideenv = {
|
|
263
373
|
**os.environ,
|
|
264
|
-
"
|
|
374
|
+
"PROMPT_COMMAND": PROMPT_COMMAND,
|
|
265
375
|
"TMPDIR": get_tmpdir(),
|
|
266
376
|
"TERM": "xterm-256color",
|
|
377
|
+
"IN_WCGW_ENVIRONMENT": "1",
|
|
378
|
+
"GIT_PAGER": "cat",
|
|
379
|
+
"PAGER": "cat",
|
|
267
380
|
}
|
|
268
381
|
try:
|
|
269
382
|
shell = pexpect.spawn(
|
|
@@ -277,7 +390,7 @@ def start_shell(
|
|
|
277
390
|
dimensions=(500, 160),
|
|
278
391
|
)
|
|
279
392
|
shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
|
|
280
|
-
shell.expect(PROMPT_CONST, timeout=
|
|
393
|
+
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
281
394
|
except Exception as e:
|
|
282
395
|
console.print(traceback.format_exc())
|
|
283
396
|
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
@@ -291,30 +404,31 @@ def start_shell(
|
|
|
291
404
|
codec_errors="backslashreplace",
|
|
292
405
|
)
|
|
293
406
|
shell.sendline(PROMPT_STATEMENT)
|
|
294
|
-
shell.expect(PROMPT_CONST, timeout=
|
|
407
|
+
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
295
408
|
|
|
296
|
-
|
|
409
|
+
initialdir_hash = md5(
|
|
410
|
+
os.path.normpath(os.path.abspath(initial_dir)).encode()
|
|
411
|
+
).hexdigest()[:5]
|
|
412
|
+
shellid = shlex.quote(
|
|
413
|
+
"wcgw."
|
|
414
|
+
+ time.strftime("%d-%Hh%Mm%Ss")
|
|
415
|
+
+ f".{initialdir_hash[:3]}."
|
|
416
|
+
+ os.path.basename(initial_dir)
|
|
417
|
+
)
|
|
297
418
|
if over_screen:
|
|
298
419
|
if not check_if_screen_command_available():
|
|
299
420
|
raise ValueError("Screen command not available")
|
|
300
421
|
# shellid is just hour, minute, second number
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
422
|
+
while True:
|
|
423
|
+
output = shell.expect([PROMPT_CONST, pexpect.TIMEOUT], timeout=0.1)
|
|
424
|
+
if output == 1:
|
|
425
|
+
break
|
|
426
|
+
shell.sendline(f"screen -q -S {shellid} {shell_path}")
|
|
305
427
|
shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
|
|
306
428
|
|
|
307
429
|
return shell, shellid
|
|
308
430
|
|
|
309
431
|
|
|
310
|
-
def _is_int(mystr: str) -> bool:
|
|
311
|
-
try:
|
|
312
|
-
int(mystr)
|
|
313
|
-
return True
|
|
314
|
-
except ValueError:
|
|
315
|
-
return False
|
|
316
|
-
|
|
317
|
-
|
|
318
432
|
def render_terminal_output(text: str) -> list[str]:
|
|
319
433
|
screen = pyte.Screen(160, 500)
|
|
320
434
|
screen.set_mode(pyte.modes.LNM)
|
|
@@ -390,8 +504,9 @@ class BashState:
|
|
|
390
504
|
use_screen: bool,
|
|
391
505
|
whitelist_for_overwrite: Optional[dict[str, "FileWhitelistData"]] = None,
|
|
392
506
|
thread_id: Optional[str] = None,
|
|
507
|
+
shell_path: Optional[str] = None,
|
|
393
508
|
) -> None:
|
|
394
|
-
self.
|
|
509
|
+
self.last_command: str = ""
|
|
395
510
|
self.console = console
|
|
396
511
|
self._cwd = working_dir or os.getcwd()
|
|
397
512
|
# Store the workspace root separately from the current working directory
|
|
@@ -403,7 +518,7 @@ class BashState:
|
|
|
403
518
|
self._write_if_empty_mode: WriteIfEmptyMode = (
|
|
404
519
|
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
405
520
|
)
|
|
406
|
-
self._mode = mode or "wcgw"
|
|
521
|
+
self._mode: Modes = mode or "wcgw"
|
|
407
522
|
self._whitelist_for_overwrite: dict[str, FileWhitelistData] = (
|
|
408
523
|
whitelist_for_overwrite or {}
|
|
409
524
|
)
|
|
@@ -414,31 +529,78 @@ class BashState:
|
|
|
414
529
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
415
530
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
416
531
|
self._use_screen = use_screen
|
|
532
|
+
# Ensure shell_path is always a str, never None
|
|
533
|
+
self._shell_path: str = (
|
|
534
|
+
shell_path if shell_path else os.environ.get("SHELL", "/bin/bash")
|
|
535
|
+
)
|
|
536
|
+
if get_rc_file_path(self._shell_path) is None:
|
|
537
|
+
console.log(
|
|
538
|
+
f"Warning: Unsupported shell: {self._shell_path}, defaulting to /bin/bash"
|
|
539
|
+
)
|
|
540
|
+
self._shell_path = "/bin/bash"
|
|
541
|
+
|
|
542
|
+
self.background_shells = dict[str, BashState]()
|
|
417
543
|
self._init_shell()
|
|
418
544
|
|
|
419
|
-
def
|
|
545
|
+
def start_new_bg_shell(self, working_dir: str) -> "BashState":
|
|
546
|
+
cid = uuid4().hex[:10]
|
|
547
|
+
state = BashState(
|
|
548
|
+
self.console,
|
|
549
|
+
working_dir=working_dir,
|
|
550
|
+
bash_command_mode=self.bash_command_mode,
|
|
551
|
+
file_edit_mode=self.file_edit_mode,
|
|
552
|
+
write_if_empty_mode=self.write_if_empty_mode,
|
|
553
|
+
mode=self.mode,
|
|
554
|
+
use_screen=self.over_screen,
|
|
555
|
+
whitelist_for_overwrite=None,
|
|
556
|
+
thread_id=cid,
|
|
557
|
+
shell_path=self._shell_path,
|
|
558
|
+
)
|
|
559
|
+
self.background_shells[cid] = state
|
|
560
|
+
return state
|
|
561
|
+
|
|
562
|
+
def expect(
|
|
563
|
+
self, pattern: Any, timeout: Optional[float] = -1, flush_rem_prompt: bool = True
|
|
564
|
+
) -> int:
|
|
420
565
|
self.close_bg_expect_thread()
|
|
421
566
|
try:
|
|
422
567
|
output = self._shell.expect(pattern, timeout)
|
|
568
|
+
if isinstance(self._shell.match, re.Match) and self._shell.match.groups():
|
|
569
|
+
cwd = self._shell.match.group(1)
|
|
570
|
+
if cwd.strip():
|
|
571
|
+
self._cwd = cwd
|
|
572
|
+
# We can safely flush current prompt
|
|
573
|
+
if flush_rem_prompt:
|
|
574
|
+
temp_before = self._shell.before
|
|
575
|
+
self.flush_prompt()
|
|
576
|
+
self._shell.before = temp_before
|
|
423
577
|
except pexpect.TIMEOUT:
|
|
424
578
|
# Edge case: gets raised when the child fd is not ready in some timeout
|
|
425
579
|
# pexpect/utils.py:143
|
|
426
580
|
return 1
|
|
427
581
|
return output
|
|
428
582
|
|
|
583
|
+
def flush_prompt(self) -> None:
|
|
584
|
+
# Flush remaining prompt
|
|
585
|
+
for _ in range(200):
|
|
586
|
+
try:
|
|
587
|
+
output = self.expect([" ", pexpect.TIMEOUT], 0.1)
|
|
588
|
+
if output == 1:
|
|
589
|
+
return
|
|
590
|
+
except pexpect.TIMEOUT:
|
|
591
|
+
return
|
|
592
|
+
|
|
429
593
|
def send(self, s: str | bytes, set_as_command: Optional[str]) -> int:
|
|
430
|
-
self.close_bg_expect_thread()
|
|
431
594
|
if set_as_command is not None:
|
|
432
|
-
self.
|
|
595
|
+
self.last_command = set_as_command
|
|
433
596
|
# if s == "\n":
|
|
434
597
|
# return self._shell.sendcontrol("m")
|
|
435
598
|
output = self._shell.send(s)
|
|
436
599
|
return output
|
|
437
600
|
|
|
438
601
|
def sendline(self, s: str | bytes, set_as_command: Optional[str]) -> int:
|
|
439
|
-
self.close_bg_expect_thread()
|
|
440
602
|
if set_as_command is not None:
|
|
441
|
-
self.
|
|
603
|
+
self.last_command = set_as_command
|
|
442
604
|
output = self._shell.sendline(s)
|
|
443
605
|
return output
|
|
444
606
|
|
|
@@ -453,8 +615,8 @@ class BashState:
|
|
|
453
615
|
@property
|
|
454
616
|
def before(self) -> Optional[str]:
|
|
455
617
|
before = self._shell.before
|
|
456
|
-
if before and before.startswith(self.
|
|
457
|
-
return before[len(self.
|
|
618
|
+
if before and before.startswith(self.last_command):
|
|
619
|
+
return before[len(self.last_command) :]
|
|
458
620
|
return before
|
|
459
621
|
|
|
460
622
|
def run_bg_expect_thread(self) -> None:
|
|
@@ -477,6 +639,8 @@ class BashState:
|
|
|
477
639
|
target=_bg_expect_thread_handler,
|
|
478
640
|
)
|
|
479
641
|
self._bg_expect_thread.start()
|
|
642
|
+
for k, v in self.background_shells.items():
|
|
643
|
+
v.run_bg_expect_thread()
|
|
480
644
|
|
|
481
645
|
def close_bg_expect_thread(self) -> None:
|
|
482
646
|
if self._bg_expect_thread:
|
|
@@ -484,9 +648,10 @@ class BashState:
|
|
|
484
648
|
self._bg_expect_thread.join()
|
|
485
649
|
self._bg_expect_thread = None
|
|
486
650
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
651
|
+
for k, v in self.background_shells.items():
|
|
652
|
+
v.close_bg_expect_thread()
|
|
487
653
|
|
|
488
654
|
def cleanup(self) -> None:
|
|
489
|
-
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
490
655
|
self.close_bg_expect_thread()
|
|
491
656
|
self._shell.close(True)
|
|
492
657
|
|
|
@@ -512,45 +677,15 @@ class BashState:
|
|
|
512
677
|
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
513
678
|
return self._write_if_empty_mode
|
|
514
679
|
|
|
515
|
-
def ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
516
|
-
quick_timeout = 0.2 if not self.over_screen else 1
|
|
517
|
-
# First reset the prompt in case venv was sourced or other reasons.
|
|
518
|
-
self.sendline(PROMPT_STATEMENT, set_as_command=PROMPT_STATEMENT)
|
|
519
|
-
self.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
520
|
-
# Reset echo also if it was enabled
|
|
521
|
-
command = "jobs | wc -l"
|
|
522
|
-
self.sendline(command, set_as_command=command)
|
|
523
|
-
before = ""
|
|
524
|
-
counts = 0
|
|
525
|
-
while not _is_int(before): # Consume all previous output
|
|
526
|
-
try:
|
|
527
|
-
self.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
528
|
-
except pexpect.TIMEOUT:
|
|
529
|
-
self.console.print(f"Couldn't get exit code, before: {before}")
|
|
530
|
-
raise
|
|
531
|
-
|
|
532
|
-
before_val = self.before
|
|
533
|
-
if not isinstance(before_val, str):
|
|
534
|
-
before_val = str(before_val)
|
|
535
|
-
assert isinstance(before_val, str)
|
|
536
|
-
before_lines = render_terminal_output(before_val)
|
|
537
|
-
before = "\n".join(before_lines).replace(command, "").strip()
|
|
538
|
-
counts += 1
|
|
539
|
-
if counts > 100:
|
|
540
|
-
raise ValueError(
|
|
541
|
-
"Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
|
|
542
|
-
)
|
|
543
|
-
try:
|
|
544
|
-
return int(before)
|
|
545
|
-
except ValueError:
|
|
546
|
-
raise ValueError(f"Malformed output: {before}")
|
|
547
|
-
|
|
548
680
|
def _init_shell(self) -> None:
|
|
549
681
|
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
550
|
-
self.
|
|
682
|
+
self.last_command = ""
|
|
551
683
|
# Ensure self._cwd exists
|
|
552
684
|
os.makedirs(self._cwd, exist_ok=True)
|
|
553
685
|
|
|
686
|
+
# Ensure WCGW block exists in rc file
|
|
687
|
+
ensure_wcgw_block_in_rc_file(self._shell_path, self.console)
|
|
688
|
+
|
|
554
689
|
# Clean up orphaned WCGW screen sessions
|
|
555
690
|
if check_if_screen_command_available():
|
|
556
691
|
cleanup_orphaned_wcgw_screens(self.console)
|
|
@@ -561,6 +696,7 @@ class BashState:
|
|
|
561
696
|
self._cwd,
|
|
562
697
|
self.console,
|
|
563
698
|
over_screen=self._use_screen,
|
|
699
|
+
shell_path=self._shell_path,
|
|
564
700
|
)
|
|
565
701
|
self.over_screen = self._use_screen
|
|
566
702
|
except Exception as e:
|
|
@@ -573,14 +709,11 @@ class BashState:
|
|
|
573
709
|
self._cwd,
|
|
574
710
|
self.console,
|
|
575
711
|
over_screen=False,
|
|
712
|
+
shell_path=self._shell_path,
|
|
576
713
|
)
|
|
577
714
|
self.over_screen = False
|
|
578
715
|
|
|
579
716
|
self._pending_output = ""
|
|
580
|
-
try:
|
|
581
|
-
self.ensure_env_and_bg_jobs()
|
|
582
|
-
except ValueError as e:
|
|
583
|
-
self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
|
|
584
717
|
|
|
585
718
|
self.run_bg_expect_thread()
|
|
586
719
|
|
|
@@ -592,7 +725,40 @@ class BashState:
|
|
|
592
725
|
def set_repl(self) -> None:
|
|
593
726
|
self._state = "repl"
|
|
594
727
|
self._pending_output = ""
|
|
595
|
-
self.
|
|
728
|
+
self.last_command = ""
|
|
729
|
+
|
|
730
|
+
def clear_to_run(self) -> None:
|
|
731
|
+
"""Check if prompt is clear to enter new command otherwise send ctrl c"""
|
|
732
|
+
# First clear
|
|
733
|
+
starttime = time.time()
|
|
734
|
+
self.close_bg_expect_thread()
|
|
735
|
+
try:
|
|
736
|
+
while True:
|
|
737
|
+
try:
|
|
738
|
+
output = self.expect(
|
|
739
|
+
[PROMPT_CONST, pexpect.TIMEOUT], 0.1, flush_rem_prompt=False
|
|
740
|
+
)
|
|
741
|
+
if output == 1:
|
|
742
|
+
break
|
|
743
|
+
except pexpect.TIMEOUT:
|
|
744
|
+
break
|
|
745
|
+
if time.time() - starttime > CONFIG.timeout:
|
|
746
|
+
self.console.log(
|
|
747
|
+
f"Error: could not clear output in {CONFIG.timeout} seconds. Resetting"
|
|
748
|
+
)
|
|
749
|
+
self.reset_shell()
|
|
750
|
+
return
|
|
751
|
+
output = self.expect([" ", pexpect.TIMEOUT], 0.1)
|
|
752
|
+
if output != 1:
|
|
753
|
+
# Then we got something new send ctrl-c
|
|
754
|
+
self.send("\x03", None)
|
|
755
|
+
|
|
756
|
+
output = self.expect([PROMPT_CONST, pexpect.TIMEOUT], CONFIG.timeout)
|
|
757
|
+
if output == 1:
|
|
758
|
+
self.console.log("Error: could not clear output. Resetting")
|
|
759
|
+
self.reset_shell()
|
|
760
|
+
finally:
|
|
761
|
+
self.run_bg_expect_thread()
|
|
596
762
|
|
|
597
763
|
@property
|
|
598
764
|
def state(self) -> BASH_CLF_OUTPUT:
|
|
@@ -614,22 +780,9 @@ class BashState:
|
|
|
614
780
|
self._workspace_root = workspace_root
|
|
615
781
|
|
|
616
782
|
@property
|
|
617
|
-
def prompt(self) -> str:
|
|
783
|
+
def prompt(self) -> re.Pattern[str]:
|
|
618
784
|
return PROMPT_CONST
|
|
619
785
|
|
|
620
|
-
def update_cwd(self) -> str:
|
|
621
|
-
self.sendline("pwd", set_as_command="pwd")
|
|
622
|
-
self.expect(PROMPT_CONST, timeout=0.2)
|
|
623
|
-
before_val = self.before
|
|
624
|
-
if not isinstance(before_val, str):
|
|
625
|
-
before_val = str(before_val)
|
|
626
|
-
before_lines = render_terminal_output(before_val)
|
|
627
|
-
current_dir = "\n".join(before_lines).strip()
|
|
628
|
-
if current_dir.startswith("pwd"):
|
|
629
|
-
current_dir = current_dir[3:].strip()
|
|
630
|
-
self._cwd = current_dir
|
|
631
|
-
return current_dir
|
|
632
|
-
|
|
633
786
|
def reset_shell(self) -> None:
|
|
634
787
|
self.cleanup()
|
|
635
788
|
self._init_shell()
|
|
@@ -964,19 +1117,21 @@ def _incremental_text(text: str, last_pending_output: str) -> str:
|
|
|
964
1117
|
return rstrip(rendered)
|
|
965
1118
|
|
|
966
1119
|
|
|
967
|
-
def get_status(bash_state: BashState) -> str:
|
|
1120
|
+
def get_status(bash_state: BashState, is_bg: bool) -> str:
|
|
968
1121
|
status = "\n\n---\n\n"
|
|
1122
|
+
if is_bg:
|
|
1123
|
+
status += f"bg_command_id = {bash_state.current_thread_id}\n"
|
|
969
1124
|
if bash_state.state == "pending":
|
|
970
1125
|
status += "status = still running\n"
|
|
971
1126
|
status += "running for = " + bash_state.get_pending_for() + "\n"
|
|
972
1127
|
status += "cwd = " + bash_state.cwd + "\n"
|
|
973
1128
|
else:
|
|
974
|
-
bg_jobs = bash_state.ensure_env_and_bg_jobs()
|
|
975
1129
|
bg_desc = ""
|
|
976
|
-
if bg_jobs and bg_jobs > 0:
|
|
977
|
-
bg_desc = f"; {bg_jobs} background jobs running"
|
|
978
1130
|
status += "status = process exited" + bg_desc + "\n"
|
|
979
|
-
status += "cwd = " + bash_state.
|
|
1131
|
+
status += "cwd = " + bash_state.cwd + "\n"
|
|
1132
|
+
|
|
1133
|
+
if not is_bg:
|
|
1134
|
+
status += "This is the main shell. " + get_bg_running_commandsinfo(bash_state)
|
|
980
1135
|
|
|
981
1136
|
return status.rstrip()
|
|
982
1137
|
|
|
@@ -1022,9 +1177,47 @@ def execute_bash(
|
|
|
1022
1177
|
|
|
1023
1178
|
finally:
|
|
1024
1179
|
bash_state.run_bg_expect_thread()
|
|
1180
|
+
if bash_state.over_screen:
|
|
1181
|
+
thread = threading.Thread(
|
|
1182
|
+
target=cleanup_orphaned_wcgw_screens,
|
|
1183
|
+
args=(bash_state.console,),
|
|
1184
|
+
daemon=True,
|
|
1185
|
+
)
|
|
1186
|
+
thread.start()
|
|
1025
1187
|
return output, cost
|
|
1026
1188
|
|
|
1027
1189
|
|
|
1190
|
+
def assert_single_statement(command: str) -> None:
|
|
1191
|
+
# Check for multiple statements using the bash statement parser
|
|
1192
|
+
if "\n" in command:
|
|
1193
|
+
try:
|
|
1194
|
+
parser = BashStatementParser()
|
|
1195
|
+
statements = parser.parse_string(command)
|
|
1196
|
+
except Exception:
|
|
1197
|
+
# Fall back to simple newline check if something goes wrong
|
|
1198
|
+
raise ValueError(
|
|
1199
|
+
"Command should not contain newline character in middle. Run only one command at a time."
|
|
1200
|
+
)
|
|
1201
|
+
if len(statements) > 1:
|
|
1202
|
+
raise ValueError(
|
|
1203
|
+
"Error: Command contains multiple statements. Please run only one bash statement at a time."
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
def get_bg_running_commandsinfo(bash_state: BashState) -> str:
|
|
1208
|
+
msg = ""
|
|
1209
|
+
running = []
|
|
1210
|
+
for id_, state in bash_state.background_shells.items():
|
|
1211
|
+
running.append(f"Command: {state.last_command}, bg_command_id: {id_}")
|
|
1212
|
+
if running:
|
|
1213
|
+
msg = (
|
|
1214
|
+
"Following background commands are attached:\n" + "\n".join(running) + "\n"
|
|
1215
|
+
)
|
|
1216
|
+
else:
|
|
1217
|
+
msg = "No command running in background.\n"
|
|
1218
|
+
return msg
|
|
1219
|
+
|
|
1220
|
+
|
|
1028
1221
|
def _execute_bash(
|
|
1029
1222
|
bash_state: BashState,
|
|
1030
1223
|
enc: EncoderDecoder[int],
|
|
@@ -1035,6 +1228,21 @@ def _execute_bash(
|
|
|
1035
1228
|
try:
|
|
1036
1229
|
is_interrupt = False
|
|
1037
1230
|
command_data = bash_arg.action_json
|
|
1231
|
+
is_bg = False
|
|
1232
|
+
og_bash_state = bash_state
|
|
1233
|
+
|
|
1234
|
+
if not isinstance(command_data, Command) and command_data.bg_command_id:
|
|
1235
|
+
if command_data.bg_command_id not in bash_state.background_shells:
|
|
1236
|
+
error = f"No shell found running with command id {command_data.bg_command_id}.\n"
|
|
1237
|
+
if bash_state.background_shells:
|
|
1238
|
+
error += get_bg_running_commandsinfo(bash_state)
|
|
1239
|
+
if bash_state.state == "pending":
|
|
1240
|
+
error += f"On the main thread a command is already running ({bash_state.last_command})"
|
|
1241
|
+
else:
|
|
1242
|
+
error += "On the main thread no command is running."
|
|
1243
|
+
raise Exception(error)
|
|
1244
|
+
bash_state = bash_state.background_shells[command_data.bg_command_id]
|
|
1245
|
+
is_bg = True
|
|
1038
1246
|
|
|
1039
1247
|
if isinstance(command_data, Command):
|
|
1040
1248
|
if bash_state.bash_command_mode.allowed_commands == "none":
|
|
@@ -1047,26 +1255,16 @@ def _execute_bash(
|
|
|
1047
1255
|
|
|
1048
1256
|
command = command_data.command.strip()
|
|
1049
1257
|
|
|
1050
|
-
|
|
1051
|
-
if "\n" in command:
|
|
1052
|
-
try:
|
|
1053
|
-
parser = BashStatementParser()
|
|
1054
|
-
statements = parser.parse_string(command)
|
|
1055
|
-
if len(statements) > 1:
|
|
1056
|
-
return (
|
|
1057
|
-
"Error: Command contains multiple statements. Please run only one bash statement at a time.",
|
|
1058
|
-
0.0,
|
|
1059
|
-
)
|
|
1060
|
-
except Exception:
|
|
1061
|
-
# Fall back to simple newline check if something goes wrong
|
|
1062
|
-
raise ValueError(
|
|
1063
|
-
"Command should not contain newline character in middle. Run only one command at a time."
|
|
1064
|
-
)
|
|
1258
|
+
assert_single_statement(command)
|
|
1065
1259
|
|
|
1066
|
-
|
|
1067
|
-
bash_state.
|
|
1068
|
-
|
|
1260
|
+
if command_data.is_background:
|
|
1261
|
+
bash_state = bash_state.start_new_bg_shell(bash_state.cwd)
|
|
1262
|
+
is_bg = True
|
|
1069
1263
|
|
|
1264
|
+
bash_state.clear_to_run()
|
|
1265
|
+
for i in range(0, len(command), 64):
|
|
1266
|
+
bash_state.send(command[i : i + 64], set_as_command=None)
|
|
1267
|
+
bash_state.send(bash_state.linesep, set_as_command=command)
|
|
1070
1268
|
elif isinstance(command_data, StatusCheck):
|
|
1071
1269
|
bash_state.console.print("Checking status")
|
|
1072
1270
|
if bash_state.state != "pending":
|
|
@@ -1100,7 +1298,7 @@ def _execute_bash(
|
|
|
1100
1298
|
elif char == "Key-right":
|
|
1101
1299
|
bash_state.send("\033[C", set_as_command=None)
|
|
1102
1300
|
elif char == "Enter":
|
|
1103
|
-
bash_state.send("\
|
|
1301
|
+
bash_state.send("\x0d", set_as_command=None)
|
|
1104
1302
|
elif char == "Ctrl-c":
|
|
1105
1303
|
bash_state.sendintr()
|
|
1106
1304
|
is_interrupt = True
|
|
@@ -1187,8 +1385,14 @@ You may want to try Ctrl-c again or program specific exit interactive commands.
|
|
|
1187
1385
|
"""
|
|
1188
1386
|
)
|
|
1189
1387
|
|
|
1190
|
-
exit_status = get_status(bash_state)
|
|
1388
|
+
exit_status = get_status(bash_state, is_bg)
|
|
1191
1389
|
incremental_text += exit_status
|
|
1390
|
+
if is_bg and bash_state.state == "repl":
|
|
1391
|
+
try:
|
|
1392
|
+
bash_state.cleanup()
|
|
1393
|
+
og_bash_state.background_shells.pop(bash_state.current_thread_id)
|
|
1394
|
+
except Exception as e:
|
|
1395
|
+
bash_state.console.log(f"error while cleaning up {e}")
|
|
1192
1396
|
|
|
1193
1397
|
return incremental_text, 0
|
|
1194
1398
|
|
|
@@ -1202,8 +1406,14 @@ You may want to try Ctrl-c again or program specific exit interactive commands.
|
|
|
1202
1406
|
output = "(...truncated)\n" + enc.decoder(tokens[-(max_tokens - 1) :])
|
|
1203
1407
|
|
|
1204
1408
|
try:
|
|
1205
|
-
exit_status = get_status(bash_state)
|
|
1409
|
+
exit_status = get_status(bash_state, is_bg)
|
|
1206
1410
|
output += exit_status
|
|
1411
|
+
if is_bg and bash_state.state == "repl":
|
|
1412
|
+
try:
|
|
1413
|
+
bash_state.cleanup()
|
|
1414
|
+
og_bash_state.background_shells.pop(bash_state.current_thread_id)
|
|
1415
|
+
except Exception as e:
|
|
1416
|
+
bash_state.console.log(f"error while cleaning up {e}")
|
|
1207
1417
|
except ValueError:
|
|
1208
1418
|
bash_state.console.print(output)
|
|
1209
1419
|
bash_state.console.print(traceback.format_exc())
|
|
@@ -59,13 +59,15 @@ def call_hello_renamed():
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
# *SEARCH/REPLACE block* Rules:
|
|
62
|
+
Every "<<<<<<< SEARCH" section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, whitespaces, etc.
|
|
63
|
+
|
|
64
|
+
Including multiple unique *SEARCH/REPLACE* blocks if needed.
|
|
65
|
+
Include enough and only enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
|
66
|
+
|
|
67
|
+
Keep *SEARCH/REPLACE* blocks concise.
|
|
68
|
+
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
|
|
69
|
+
Include just the changing lines, and a few surrounding lines (0-3 lines) if needed for uniqueness.
|
|
70
|
+
Other than for uniqueness, avoid including those lines which do not change in search (and replace) blocks. Target 0-3 non trivial extra lines per block.
|
|
71
|
+
|
|
72
|
+
Preserve leading spaces and indentations in both SEARCH and REPLACE blocks.
|
|
62
73
|
|
|
63
|
-
- Every "SEARCH" section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, whitespaces, etc.
|
|
64
|
-
- Use multiple search/replace blocks in a single FileWriteOrEdit tool call to edit in a single file in multiple places from top to bottom (separate calls are slower).
|
|
65
|
-
- Including multiple unique *SEARCH/REPLACE* blocks if needed.
|
|
66
|
-
- Include enough and only enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
|
67
|
-
- Keep *SEARCH/REPLACE* blocks concise.
|
|
68
|
-
- Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
|
|
69
|
-
- Include just the changing lines, and a few surrounding lines (0-3 lines) if needed for uniqueness.
|
|
70
|
-
- Other than for uniqueness, avoid including those lines which do not change in search (and replace) blocks. Target 0-3 non trivial extra lines per block.
|
|
71
|
-
- Preserve leading spaces and indentations in both SEARCH and REPLACE blocks.
|
|
@@ -15,6 +15,9 @@ def app(
|
|
|
15
15
|
version: bool = typer.Option(
|
|
16
16
|
False, "--version", "-v", help="Show version and exit"
|
|
17
17
|
),
|
|
18
|
+
shell: str = typer.Option(
|
|
19
|
+
"", "--shell", help="Path to shell executable (defaults to $SHELL or /bin/bash)"
|
|
20
|
+
),
|
|
18
21
|
) -> None:
|
|
19
22
|
"""Main entry point for the package."""
|
|
20
23
|
if version:
|
|
@@ -22,7 +25,7 @@ def app(
|
|
|
22
25
|
print(f"wcgw version: {version_}")
|
|
23
26
|
raise typer.Exit()
|
|
24
27
|
|
|
25
|
-
asyncio.run(server.main())
|
|
28
|
+
asyncio.run(server.main(shell))
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
# Optionally expose other important items at package level
|
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -151,7 +151,7 @@ BASH_STATE = None
|
|
|
151
151
|
CUSTOM_INSTRUCTIONS = None
|
|
152
152
|
|
|
153
153
|
|
|
154
|
-
async def main() -> None:
|
|
154
|
+
async def main(shell_path: str = "") -> None:
|
|
155
155
|
global BASH_STATE, CUSTOM_INSTRUCTIONS
|
|
156
156
|
CONFIG.update(3, 55, 5)
|
|
157
157
|
version = str(importlib.metadata.version("wcgw"))
|
|
@@ -164,7 +164,7 @@ async def main() -> None:
|
|
|
164
164
|
starting_dir = os.path.join(tmp_dir, "claude_playground")
|
|
165
165
|
|
|
166
166
|
with BashState(
|
|
167
|
-
Console(), starting_dir, None, None, None, None, True, None
|
|
167
|
+
Console(), starting_dir, None, None, None, None, True, None, None, shell_path or None
|
|
168
168
|
) as BASH_STATE:
|
|
169
169
|
BASH_STATE.console.log("wcgw version: " + version)
|
|
170
170
|
# Run the server using stdin/stdout streams
|
wcgw/client/modes.py
CHANGED
|
@@ -86,7 +86,7 @@ You are now running in "code_writer" mode.
|
|
|
86
86
|
|
|
87
87
|
run_command_common = """
|
|
88
88
|
- Do not use Ctrl-c interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
89
|
-
- Do not use echo to write
|
|
89
|
+
- Do not use echo/cat to write any file, always use FileWriteOrEdit tool to create/update files.
|
|
90
90
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
91
91
|
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
92
92
|
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using wcgw tools.
|
|
@@ -120,7 +120,8 @@ WCGW_PROMPT = """
|
|
|
120
120
|
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
121
121
|
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using wcgw tools
|
|
122
122
|
- Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
123
|
-
- Do not use echo to write
|
|
123
|
+
- Do not use echo/cat to write any file, always use FileWriteOrEdit tool to create/update files.
|
|
124
|
+
- You can share task summary directly without creating any file.
|
|
124
125
|
- Provide as many file paths as you need in ReadFiles in one go.
|
|
125
126
|
|
|
126
127
|
Additional instructions:
|
wcgw/client/tool_prompts.py
CHANGED
|
@@ -42,12 +42,14 @@ TOOL_PROMPTS = [
|
|
|
42
42
|
- Status of the command and the current working directory will always be returned at the end.
|
|
43
43
|
- The first or the last line might be `(...truncated)` if the output is too long.
|
|
44
44
|
- Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
|
|
45
|
-
-
|
|
46
|
-
-
|
|
45
|
+
- Do not run bg commands using "&", instead use this tool.
|
|
46
|
+
- You must not use echo/cat to read/write files, use ReadFiles/FileWriteOrEdit
|
|
47
47
|
- In order to check status of previous command, use `status_check` with empty command argument.
|
|
48
48
|
- Only command is allowed to run at a time. You need to wait for any previous command to finish before running a new one.
|
|
49
49
|
- Programs don't hang easily, so most likely explanation for no output is usually that the program is still running, and you need to check status again.
|
|
50
50
|
- Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
|
|
51
|
+
- Only run long running commands in background. Each background command is run in a new non-reusable shell.
|
|
52
|
+
- On running a bg command you'll get a bg command id that you should use to get status or interact.
|
|
51
53
|
""",
|
|
52
54
|
annotations=ToolAnnotations(destructiveHint=True, openWorldHint=True),
|
|
53
55
|
),
|
wcgw/client/tools.py
CHANGED
|
@@ -392,7 +392,7 @@ def reset_wcgw(
|
|
|
392
392
|
f"Reset successful with mode change to {mode_name}.\n"
|
|
393
393
|
+ mode_prompt
|
|
394
394
|
+ "\n"
|
|
395
|
-
+ get_status(context.bash_state)
|
|
395
|
+
+ get_status(context.bash_state, is_bg=False)
|
|
396
396
|
)
|
|
397
397
|
else:
|
|
398
398
|
# Regular reset without mode change - keep same mode but update directory
|
|
@@ -412,7 +412,7 @@ def reset_wcgw(
|
|
|
412
412
|
starting_directory,
|
|
413
413
|
thread_id,
|
|
414
414
|
)
|
|
415
|
-
return "Reset successful" + get_status(context.bash_state)
|
|
415
|
+
return "Reset successful" + get_status(context.bash_state, is_bg=False)
|
|
416
416
|
|
|
417
417
|
|
|
418
418
|
T = TypeVar("T")
|
|
@@ -1374,24 +1374,23 @@ if __name__ == "__main__":
|
|
|
1374
1374
|
print(
|
|
1375
1375
|
get_tool_output(
|
|
1376
1376
|
Context(BASH_STATE, BASH_STATE.console),
|
|
1377
|
-
|
|
1378
|
-
|
|
1377
|
+
BashCommand(
|
|
1378
|
+
action_json=Command(command="source .venv/bin/activate"),
|
|
1379
|
+
thread_id=BASH_STATE.current_thread_id,
|
|
1379
1380
|
),
|
|
1380
1381
|
default_enc,
|
|
1381
1382
|
0,
|
|
1382
1383
|
lambda x, y: ("", 0),
|
|
1383
1384
|
24000, # coding_max_tokens
|
|
1384
1385
|
8000, # noncoding_max_tokens
|
|
1385
|
-
)
|
|
1386
|
+
)
|
|
1386
1387
|
)
|
|
1387
1388
|
|
|
1388
1389
|
print(
|
|
1389
1390
|
get_tool_output(
|
|
1390
1391
|
Context(BASH_STATE, BASH_STATE.console),
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
text_or_search_replace_blocks="""test""",
|
|
1394
|
-
percentage_to_change=100,
|
|
1392
|
+
BashCommand(
|
|
1393
|
+
action_json=Command(command="pwd"),
|
|
1395
1394
|
thread_id=BASH_STATE.current_thread_id,
|
|
1396
1395
|
),
|
|
1397
1396
|
default_enc,
|
|
@@ -1399,5 +1398,35 @@ if __name__ == "__main__":
|
|
|
1399
1398
|
lambda x, y: ("", 0),
|
|
1400
1399
|
24000, # coding_max_tokens
|
|
1401
1400
|
8000, # noncoding_max_tokens
|
|
1402
|
-
)
|
|
1401
|
+
)
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
print(
|
|
1405
|
+
get_tool_output(
|
|
1406
|
+
Context(BASH_STATE, BASH_STATE.console),
|
|
1407
|
+
BashCommand(
|
|
1408
|
+
action_json=Command(command="take src"),
|
|
1409
|
+
thread_id=BASH_STATE.current_thread_id,
|
|
1410
|
+
),
|
|
1411
|
+
default_enc,
|
|
1412
|
+
0,
|
|
1413
|
+
lambda x, y: ("", 0),
|
|
1414
|
+
24000, # coding_max_tokens
|
|
1415
|
+
8000, # noncoding_max_tokens
|
|
1416
|
+
)
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
print(
|
|
1420
|
+
get_tool_output(
|
|
1421
|
+
Context(BASH_STATE, BASH_STATE.console),
|
|
1422
|
+
BashCommand(
|
|
1423
|
+
action_json=Command(command="pwd"),
|
|
1424
|
+
thread_id=BASH_STATE.current_thread_id,
|
|
1425
|
+
),
|
|
1426
|
+
default_enc,
|
|
1427
|
+
0,
|
|
1428
|
+
lambda x, y: ("", 0),
|
|
1429
|
+
24000, # coding_max_tokens
|
|
1430
|
+
8000, # noncoding_max_tokens
|
|
1431
|
+
)
|
|
1403
1432
|
)
|
wcgw/types_.py
CHANGED
|
@@ -86,16 +86,19 @@ class Initialize(BaseModel):
|
|
|
86
86
|
class Command(BaseModel):
|
|
87
87
|
command: str
|
|
88
88
|
type: Literal["command"] = "command"
|
|
89
|
+
is_background: bool = False
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
class StatusCheck(BaseModel):
|
|
92
93
|
status_check: Literal[True] = True
|
|
93
94
|
type: Literal["status_check"] = "status_check"
|
|
95
|
+
bg_command_id: str | None = None
|
|
94
96
|
|
|
95
97
|
|
|
96
98
|
class SendText(BaseModel):
|
|
97
99
|
send_text: str
|
|
98
100
|
type: Literal["send_text"] = "send_text"
|
|
101
|
+
bg_command_id: str | None = None
|
|
99
102
|
|
|
100
103
|
|
|
101
104
|
Specials = Literal[
|
|
@@ -106,11 +109,13 @@ Specials = Literal[
|
|
|
106
109
|
class SendSpecials(BaseModel):
|
|
107
110
|
send_specials: Sequence[Specials]
|
|
108
111
|
type: Literal["send_specials"] = "send_specials"
|
|
112
|
+
bg_command_id: str | None = None
|
|
109
113
|
|
|
110
114
|
|
|
111
115
|
class SendAscii(BaseModel):
|
|
112
116
|
send_ascii: Sequence[int]
|
|
113
117
|
type: Literal["send_ascii"] = "send_ascii"
|
|
118
|
+
bg_command_id: str | None = None
|
|
114
119
|
|
|
115
120
|
|
|
116
121
|
class ActionJsonSchema(BaseModel):
|
|
@@ -132,6 +137,14 @@ class ActionJsonSchema(BaseModel):
|
|
|
132
137
|
send_ascii: Optional[Sequence[int]] = Field(
|
|
133
138
|
default=None, description='Set only if type="send_ascii"'
|
|
134
139
|
)
|
|
140
|
+
is_background: bool = Field(
|
|
141
|
+
default=False,
|
|
142
|
+
description='Set only if type="command" and running the command in background',
|
|
143
|
+
)
|
|
144
|
+
bg_command_id: str | None = Field(
|
|
145
|
+
default=None,
|
|
146
|
+
description='Set only if type!="command" and doing action on a running background command',
|
|
147
|
+
)
|
|
135
148
|
|
|
136
149
|
|
|
137
150
|
class BashCommandOverride(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.5.1
|
|
4
4
|
Summary: Shell and coding agent for Claude and other mcp clients
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
@@ -49,6 +49,8 @@ wcgw is an MCP server with tightly integrated shell and code editing tools.
|
|
|
49
49
|
|
|
50
50
|
## Updates
|
|
51
51
|
|
|
52
|
+
- [6 Oct 2025] Model can now run multiple commands in background. ZSH is now a supported shell. Multiplexing improvements.
|
|
53
|
+
|
|
52
54
|
- [27 Apr 2025] Removed support for GPTs over relay server. Only MCP server is supported in version >= 5.
|
|
53
55
|
|
|
54
56
|
- [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
|
|
@@ -77,16 +79,16 @@ wcgw is an MCP server with tightly integrated shell and code editing tools.
|
|
|
77
79
|
- File edit has spacing tolerant matching, with warning on issues like indentation mismatch. If there's no match, the closest match is returned to the AI to fix its mistakes.
|
|
78
80
|
- Using Aider-like search and replace, which has better performance than tool call based search and replace.
|
|
79
81
|
- ⚡ **Shell optimizations**:
|
|
80
|
-
- Only one command is allowed to be run at a time, simplifying management and avoiding rogue processes. There's only single shell instance at any point of time.
|
|
81
82
|
- Current working directory is always returned after any shell command to prevent AI from getting lost.
|
|
82
83
|
- Command polling exits after a quick timeout to avoid slow feedback. However, status checking has wait tolerance based on fresh output streaming from a command. Both of these approach combined provides a good shell interaction experience.
|
|
84
|
+
- Supports multiple concurrent background commands alongside the main interactive shell.
|
|
83
85
|
- ⚡ **Saving repo context in a single file**: Task checkpointing using "ContextSave" tool saves detailed context in a single file. Tasks can later be resumed in a new chat asking "Resume `task id`". The saved file can be used to do other kinds of knowledge transfer, such as taking help from another AI.
|
|
84
86
|
- ⚡ **Easily switch between various modes**:
|
|
85
87
|
- Ask it to run in 'architect' mode for planning. Inspired by adier's architect mode, work with Claude to come up with a plan first. Leads to better accuracy and prevents premature file editing.
|
|
86
88
|
- Ask it to run in 'code-writer' mode for code editing and project building. You can provide specific paths with wild card support to prevent other files getting edited.
|
|
87
89
|
- By default it runs in 'wcgw' mode that has no restrictions and full authorisation.
|
|
88
90
|
- More details in [Modes section](#modes)
|
|
89
|
-
- ⚡ **Runs in multiplex terminal**
|
|
91
|
+
- ⚡ **Runs in multiplex terminal** Use [vscode extension](https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw) or run `screen -x` to attach to the terminal that the AI runs commands on. See history or interrupt process or interact with the same terminal that AI uses.
|
|
90
92
|
- ⚡ **Automatically load CLAUDE.md/AGENTS.md** Loads "CLAUDE.md" or "AGENTS.md" file in project root and sends as instructions during initialisation. Instructions in a global "~/.wcgw/CLAUDE.md" or "~/.wcgw/AGENTS.md" file are loaded and added along with project specific CLAUDE.md. The file name is case sensitive. CLAUDE.md is attached if it's present otherwise AGENTS.md is attached.
|
|
91
93
|
|
|
92
94
|
## Top use cases examples
|
|
@@ -118,8 +120,8 @@ Then create or update `claude_desktop_config.json` (~/Library/Application Suppor
|
|
|
118
120
|
{
|
|
119
121
|
"mcpServers": {
|
|
120
122
|
"wcgw": {
|
|
121
|
-
"command": "
|
|
122
|
-
"args": ["
|
|
123
|
+
"command": "uvx",
|
|
124
|
+
"args": ["wcgw@latest"]
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
}
|
|
@@ -127,6 +129,21 @@ Then create or update `claude_desktop_config.json` (~/Library/Application Suppor
|
|
|
127
129
|
|
|
128
130
|
Then restart claude app.
|
|
129
131
|
|
|
132
|
+
**Optional: Force a specific shell**
|
|
133
|
+
|
|
134
|
+
To use a specific shell (bash or zsh), add the `--shell` argument:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"mcpServers": {
|
|
139
|
+
"wcgw": {
|
|
140
|
+
"command": "uvx",
|
|
141
|
+
"args": ["wcgw@latest", "--shell", "/bin/bash"]
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
130
147
|
_If there's an error in setting up_
|
|
131
148
|
|
|
132
149
|
- If there's an error like "uv ENOENT", make sure `uv` is installed. Then run 'which uv' in the terminal, and use its output in place of "uv" in the configuration.
|
|
@@ -148,7 +165,7 @@ Then add or update the claude config file `%APPDATA%\Claude\claude_desktop_confi
|
|
|
148
165
|
"mcpServers": {
|
|
149
166
|
"wcgw": {
|
|
150
167
|
"command": "wsl.exe",
|
|
151
|
-
"args": ["
|
|
168
|
+
"args": ["uvx", "wcgw@latest"]
|
|
152
169
|
}
|
|
153
170
|
}
|
|
154
171
|
}
|
|
@@ -210,6 +227,9 @@ Note: in code-writer mode either all commands are allowed or none are allowed fo
|
|
|
210
227
|
|
|
211
228
|
#### Attach to the working terminal to investigate
|
|
212
229
|
|
|
230
|
+
NEW: the [vscode extension](https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw) now automatically attach the running terminal
|
|
231
|
+
if workspace path matches.
|
|
232
|
+
|
|
213
233
|
If you've `screen` command installed, wcgw runs on a screen instance automatically. If you've started wcgw mcp server, you can list the screen sessions:
|
|
214
234
|
|
|
215
235
|
`screen -ls`
|
|
@@ -220,7 +240,7 @@ You can then attach to the session using `screen -x 93358.wcgw.235521`
|
|
|
220
240
|
|
|
221
241
|
You may interrupt any running command safely.
|
|
222
242
|
|
|
223
|
-
You can interact with the terminal
|
|
243
|
+
You can interact with the terminal safely, for example for entering passwords, or entering some text. (Warning: If you run a new command, any new LLM command will interrupt it.)
|
|
224
244
|
|
|
225
245
|
You shouldn't exit the session using `exit `or Ctrl-d, instead you should use `ctrl+a+d` to safely detach without destroying the screen session.
|
|
226
246
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
wcgw/__init__.py,sha256=JgAY25VsA208v8E7QTIU0E50nsk-TCJ4FWTEHmnssYU,127
|
|
2
2
|
wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
wcgw/types_.py,sha256=
|
|
3
|
+
wcgw/types_.py,sha256=sSgmWC8ciQ7b6yarVzmYGad81h7jlPK_XyftT28HerA,10104
|
|
4
4
|
wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
wcgw/client/common.py,sha256=OCH7Tx64jojz3M3iONUrGMadE07W21DiZs5sOxWX1Qc,1456
|
|
6
|
-
wcgw/client/diff-instructions.txt,sha256=
|
|
6
|
+
wcgw/client/diff-instructions.txt,sha256=EU9WmJMbsmAj07CyuGWL6zd4eVQ8Ie3HZzd0k0WugX0,1687
|
|
7
7
|
wcgw/client/memory.py,sha256=U2Nw2si3Zg7n_RhNAuaYcmrrDtZ_Mooi-kfAOKflT-I,3079
|
|
8
|
-
wcgw/client/modes.py,sha256=
|
|
8
|
+
wcgw/client/modes.py,sha256=3eS5ttttltKAZB41h7-XwtdjMVYrUtd6xMush7k_vvw,10388
|
|
9
9
|
wcgw/client/schema_generator.py,sha256=mEIy6BgHlfJeAjJtwY_VwoIDmu-Fax2H9bVtj7IMuEo,2282
|
|
10
|
-
wcgw/client/tool_prompts.py,sha256=
|
|
11
|
-
wcgw/client/tools.py,sha256=
|
|
12
|
-
wcgw/client/bash_state/bash_state.py,sha256=
|
|
10
|
+
wcgw/client/tool_prompts.py,sha256=zi4L98DwNePr5Yhwx7wl4jAgmlMjILPnKTLZGf0L4SA,4971
|
|
11
|
+
wcgw/client/tools.py,sha256=YTjtAPSR8divbfDDOII9-iZG6B1ucPemdZ5pef6lEyQ,50904
|
|
12
|
+
wcgw/client/bash_state/bash_state.py,sha256=tVVaUwSw2st9vPEkGQPbA0Qtb4gbgjCmP1TGgSQ_hU8,48872
|
|
13
13
|
wcgw/client/bash_state/parser/__init__.py,sha256=AnlNSmoQTSoqqlLOLX4P1uXfzc5VGeCGJsGgtisq2zE,207
|
|
14
14
|
wcgw/client/bash_state/parser/bash_statement_parser.py,sha256=9a8vPO1r3_tXmaAcubTQ5UY-NseWlalgm8LZA17LXuY,6058
|
|
15
15
|
wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
|
|
@@ -17,8 +17,8 @@ wcgw/client/file_ops/diff_edit.py,sha256=AwLq6-pY7czv1y-JA5O2Q4rgbvn82YmSL9jD8XB
|
|
|
17
17
|
wcgw/client/file_ops/extensions.py,sha256=CmfD7ON6SY24Prh2tRZdV9KbhuOrWqqk8qL1VtshzB8,3608
|
|
18
18
|
wcgw/client/file_ops/search_replace.py,sha256=5LFg-_U_ijnNrkYei4SWCPGKPGgDzJs49EDsIBzLmuY,6822
|
|
19
19
|
wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
|
|
20
|
-
wcgw/client/mcp_server/__init__.py,sha256=
|
|
21
|
-
wcgw/client/mcp_server/server.py,sha256=
|
|
20
|
+
wcgw/client/mcp_server/__init__.py,sha256=be5D_r2HNhWAi8uX2qz28z1hSjh_rfUDqqeBVyEyzkk,753
|
|
21
|
+
wcgw/client/mcp_server/server.py,sha256=qREy9CQckvpLYx1VaujDljK6eTQSLA53C8KuK8dkU3U,5571
|
|
22
22
|
wcgw/client/repo_ops/display_tree.py,sha256=g282qCKLCwo8O9NHUBnkG_NkIusroVzz3NZi8VIcmAI,4066
|
|
23
23
|
wcgw/client/repo_ops/file_stats.py,sha256=AUA0Br7zFRpylWFYZPGMeGPJy3nWp9e2haKi34JptHE,4887
|
|
24
24
|
wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
|
|
@@ -27,12 +27,12 @@ wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5
|
|
|
27
27
|
wcgw/client/repo_ops/repo_context.py,sha256=e_w-1VfxWQiZT3r66N13nlmPt6AGm0uvG3A7aYSgaCI,9632
|
|
28
28
|
wcgw_cli/__init__.py,sha256=TNxXsTPgb52OhakIda9wTRh91cqoBqgQRx5TxjzQQFU,21
|
|
29
29
|
wcgw_cli/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
30
|
-
wcgw_cli/anthropic_client.py,sha256=
|
|
30
|
+
wcgw_cli/anthropic_client.py,sha256=Si3u3K3OWe0gNBmwh892QG34yRirUQkhWKXHypg4tZM,19888
|
|
31
31
|
wcgw_cli/cli.py,sha256=-7FBe_lahKyUOhf65iurTA1M1gXXXAiT0OVKQVcZKKo,948
|
|
32
|
-
wcgw_cli/openai_client.py,sha256=
|
|
32
|
+
wcgw_cli/openai_client.py,sha256=QBRI_LIGI04iBSHZIXv5cGIFaRrPXZLwRgqnDf34uWc,15992
|
|
33
33
|
wcgw_cli/openai_utils.py,sha256=xGOb3W5ALrIozV7oszfGYztpj0FnXdD7jAxm5lEIVKY,2439
|
|
34
|
-
wcgw-5.
|
|
35
|
-
wcgw-5.
|
|
36
|
-
wcgw-5.
|
|
37
|
-
wcgw-5.
|
|
38
|
-
wcgw-5.
|
|
34
|
+
wcgw-5.5.1.dist-info/METADATA,sha256=j-jNe7lVsoSLGlT_xsh1Sqq2G4j8GbePY7FmOvsSeoI,16385
|
|
35
|
+
wcgw-5.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
36
|
+
wcgw-5.5.1.dist-info/entry_points.txt,sha256=UnjK-MAH4Qssh0tGJDMeij1oi-oRKokItkknP_BwShE,94
|
|
37
|
+
wcgw-5.5.1.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
38
|
+
wcgw-5.5.1.dist-info/RECORD,,
|
wcgw_cli/anthropic_client.py
CHANGED
wcgw_cli/openai_client.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|