wcgw 1.4.0__py3-none-any.whl → 1.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/__main__.py +2 -2
- wcgw/client/anthropic_client.py +83 -37
- wcgw/client/computer_use.py +415 -0
- wcgw/client/mcp_server/Readme.md +50 -4
- wcgw/client/mcp_server/server.py +117 -54
- wcgw/client/openai_client.py +3 -2
- wcgw/client/sys_utils.py +41 -0
- wcgw/client/tools.py +186 -80
- wcgw/types_.py +41 -0
- {wcgw-1.4.0.dist-info → wcgw-1.5.1.dist-info}/METADATA +73 -26
- wcgw-1.5.1.dist-info/RECORD +22 -0
- wcgw-1.4.0.dist-info/RECORD +0 -20
- {wcgw-1.4.0.dist-info → wcgw-1.5.1.dist-info}/WHEEL +0 -0
- {wcgw-1.4.0.dist-info → wcgw-1.5.1.dist-info}/entry_points.txt +0 -0
wcgw/client/tools.py
CHANGED
|
@@ -11,6 +11,7 @@ import threading
|
|
|
11
11
|
import importlib.metadata
|
|
12
12
|
import time
|
|
13
13
|
import traceback
|
|
14
|
+
from tempfile import TemporaryDirectory
|
|
14
15
|
from typing import (
|
|
15
16
|
Callable,
|
|
16
17
|
Literal,
|
|
@@ -24,6 +25,7 @@ from typing import (
|
|
|
24
25
|
import uuid
|
|
25
26
|
from pydantic import BaseModel, TypeAdapter
|
|
26
27
|
import typer
|
|
28
|
+
from .computer_use import run_computer_tool
|
|
27
29
|
from websockets.sync.client import connect as syncconnect
|
|
28
30
|
|
|
29
31
|
import os
|
|
@@ -57,15 +59,19 @@ from ..types_ import (
|
|
|
57
59
|
ReadFile,
|
|
58
60
|
ReadImage,
|
|
59
61
|
ResetShell,
|
|
62
|
+
Mouse,
|
|
63
|
+
Keyboard,
|
|
64
|
+
ScreenShot,
|
|
65
|
+
GetScreenInfo,
|
|
60
66
|
)
|
|
61
67
|
|
|
62
68
|
from .common import CostData, Models, discard_input
|
|
63
|
-
|
|
69
|
+
from .sys_utils import command_run
|
|
64
70
|
from .openai_utils import get_input_cost, get_output_cost
|
|
65
71
|
|
|
66
72
|
console = rich.console.Console(style="magenta", highlight=False, markup=False)
|
|
67
73
|
|
|
68
|
-
TIMEOUT =
|
|
74
|
+
TIMEOUT = 5
|
|
69
75
|
|
|
70
76
|
|
|
71
77
|
def render_terminal_output(text: str) -> str:
|
|
@@ -107,9 +113,9 @@ def start_shell() -> pexpect.spawn: # type: ignore
|
|
|
107
113
|
encoding="utf-8",
|
|
108
114
|
timeout=TIMEOUT,
|
|
109
115
|
)
|
|
110
|
-
SHELL.expect(PROMPT)
|
|
116
|
+
SHELL.expect(PROMPT, timeout=TIMEOUT)
|
|
111
117
|
SHELL.sendline("stty -icanon -echo")
|
|
112
|
-
SHELL.expect(PROMPT)
|
|
118
|
+
SHELL.expect(PROMPT, timeout=TIMEOUT)
|
|
113
119
|
return SHELL
|
|
114
120
|
|
|
115
121
|
|
|
@@ -129,15 +135,15 @@ def _get_exit_code() -> int:
|
|
|
129
135
|
return 0
|
|
130
136
|
# First reset the prompt in case venv was sourced or other reasons.
|
|
131
137
|
SHELL.sendline(f"export PS1={PROMPT}")
|
|
132
|
-
SHELL.expect(PROMPT)
|
|
138
|
+
SHELL.expect(PROMPT, timeout=0.2)
|
|
133
139
|
# Reset echo also if it was enabled
|
|
134
140
|
SHELL.sendline("stty -icanon -echo")
|
|
135
|
-
SHELL.expect(PROMPT)
|
|
141
|
+
SHELL.expect(PROMPT, timeout=0.2)
|
|
136
142
|
SHELL.sendline("echo $?")
|
|
137
143
|
before = ""
|
|
138
144
|
while not _is_int(before): # Consume all previous output
|
|
139
145
|
try:
|
|
140
|
-
SHELL.expect(PROMPT)
|
|
146
|
+
SHELL.expect(PROMPT, timeout=0.2)
|
|
141
147
|
except pexpect.TIMEOUT:
|
|
142
148
|
print(f"Couldn't get exit code, before: {before}")
|
|
143
149
|
raise
|
|
@@ -153,6 +159,7 @@ def _get_exit_code() -> int:
|
|
|
153
159
|
|
|
154
160
|
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
155
161
|
BASH_STATE: BASH_CLF_OUTPUT = "repl"
|
|
162
|
+
IS_IN_DOCKER: Optional[str] = ""
|
|
156
163
|
CWD = os.getcwd()
|
|
157
164
|
|
|
158
165
|
|
|
@@ -167,10 +174,11 @@ Current working directory: {CWD}
|
|
|
167
174
|
|
|
168
175
|
|
|
169
176
|
def reset_shell() -> str:
|
|
170
|
-
global SHELL, BASH_STATE, CWD
|
|
177
|
+
global SHELL, BASH_STATE, CWD, IS_IN_DOCKER
|
|
171
178
|
SHELL.close(True)
|
|
172
179
|
SHELL = start_shell()
|
|
173
180
|
BASH_STATE = "repl"
|
|
181
|
+
IS_IN_DOCKER = ""
|
|
174
182
|
CWD = os.getcwd()
|
|
175
183
|
return "Reset successful" + get_status()
|
|
176
184
|
|
|
@@ -207,7 +215,7 @@ def update_repl_prompt(command: str) -> bool:
|
|
|
207
215
|
|
|
208
216
|
def get_cwd() -> str:
|
|
209
217
|
SHELL.sendline("pwd")
|
|
210
|
-
SHELL.expect(PROMPT)
|
|
218
|
+
SHELL.expect(PROMPT, timeout=0.2)
|
|
211
219
|
assert isinstance(SHELL.before, str)
|
|
212
220
|
current_dir = render_terminal_output(SHELL.before).strip()
|
|
213
221
|
return current_dir
|
|
@@ -234,6 +242,7 @@ def execute_bash(
|
|
|
234
242
|
enc: tiktoken.Encoding,
|
|
235
243
|
bash_arg: BashCommand | BashInteraction,
|
|
236
244
|
max_tokens: Optional[int],
|
|
245
|
+
timeout_s: Optional[float],
|
|
237
246
|
) -> tuple[str, float]:
|
|
238
247
|
global SHELL, BASH_STATE, CWD
|
|
239
248
|
try:
|
|
@@ -333,13 +342,13 @@ def execute_bash(
|
|
|
333
342
|
SHELL.expect(PROMPT)
|
|
334
343
|
return "---\n\nFailure: user interrupted the execution", 0.0
|
|
335
344
|
|
|
336
|
-
wait =
|
|
345
|
+
wait = timeout_s or TIMEOUT
|
|
337
346
|
index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=wait)
|
|
338
347
|
if index == 1:
|
|
339
348
|
BASH_STATE = "pending"
|
|
340
349
|
text = SHELL.before or ""
|
|
341
350
|
|
|
342
|
-
text = render_terminal_output(text)
|
|
351
|
+
text = render_terminal_output(text[-100_000:])
|
|
343
352
|
tokens = enc.encode(text)
|
|
344
353
|
|
|
345
354
|
if max_tokens and len(tokens) >= max_tokens:
|
|
@@ -438,14 +447,26 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
438
447
|
if not os.path.isabs(file_path):
|
|
439
448
|
file_path = os.path.join(CWD, file_path)
|
|
440
449
|
|
|
441
|
-
if not
|
|
442
|
-
|
|
450
|
+
if not IS_IN_DOCKER:
|
|
451
|
+
if not os.path.exists(file_path):
|
|
452
|
+
raise ValueError(f"File {file_path} does not exist")
|
|
443
453
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
454
|
+
with open(file_path, "rb") as image_file:
|
|
455
|
+
image_bytes = image_file.read()
|
|
456
|
+
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
457
|
+
image_type = mimetypes.guess_type(file_path)[0]
|
|
458
|
+
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
459
|
+
else:
|
|
460
|
+
with TemporaryDirectory() as tmpdir:
|
|
461
|
+
rcode = os.system(f"docker cp {IS_IN_DOCKER}:{file_path} {tmpdir}")
|
|
462
|
+
if rcode != 0:
|
|
463
|
+
raise Exception(f"Error: Read failed with code {rcode}")
|
|
464
|
+
path_ = os.path.join(tmpdir, os.path.basename(file_path))
|
|
465
|
+
with open(path_, "rb") as f:
|
|
466
|
+
image_bytes = f.read()
|
|
467
|
+
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
468
|
+
image_type = mimetypes.guess_type(file_path)[0]
|
|
469
|
+
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
449
470
|
|
|
450
471
|
|
|
451
472
|
def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
@@ -454,19 +475,42 @@ def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
|
454
475
|
else:
|
|
455
476
|
path_ = writefile.file_path
|
|
456
477
|
|
|
457
|
-
if
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
478
|
+
if not IS_IN_DOCKER:
|
|
479
|
+
if error_on_exist and os.path.exists(path_):
|
|
480
|
+
file_data = Path(path_).read_text()
|
|
481
|
+
if file_data:
|
|
482
|
+
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
461
483
|
|
|
462
|
-
|
|
463
|
-
|
|
484
|
+
path = Path(path_)
|
|
485
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
with path.open("w") as f:
|
|
489
|
+
f.write(writefile.file_content)
|
|
490
|
+
except OSError as e:
|
|
491
|
+
return f"Error: {e}"
|
|
492
|
+
else:
|
|
493
|
+
if error_on_exist:
|
|
494
|
+
# Check if it exists using os.system
|
|
495
|
+
cmd = f"test -f {path_}"
|
|
496
|
+
status = os.system(f'docker exec {IS_IN_DOCKER} bash -c "{cmd}"')
|
|
497
|
+
if status == 0:
|
|
498
|
+
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
499
|
+
|
|
500
|
+
with TemporaryDirectory() as tmpdir:
|
|
501
|
+
tmppath = os.path.join(tmpdir, os.path.basename(path_))
|
|
502
|
+
with open(tmppath, "w") as f:
|
|
503
|
+
f.write(writefile.file_content)
|
|
504
|
+
os.chmod(tmppath, 0o777)
|
|
505
|
+
parent_dir = os.path.dirname(path_)
|
|
506
|
+
rcode = os.system(f"docker exec {IS_IN_DOCKER} mkdir -p {parent_dir}")
|
|
507
|
+
if rcode != 0:
|
|
508
|
+
return f"Error: Write failed with code while creating dirs {rcode}"
|
|
509
|
+
|
|
510
|
+
rcode = os.system(f"docker cp {tmppath} {IS_IN_DOCKER}:{path_}")
|
|
511
|
+
if rcode != 0:
|
|
512
|
+
return f"Error: Write failed with code {rcode}"
|
|
464
513
|
|
|
465
|
-
try:
|
|
466
|
-
with path.open("w") as f:
|
|
467
|
-
f.write(writefile.file_content)
|
|
468
|
-
except OSError as e:
|
|
469
|
-
return f"Error: {e}"
|
|
470
514
|
console.print(f"File written to {path_}")
|
|
471
515
|
return "Success"
|
|
472
516
|
|
|
@@ -545,11 +589,21 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
545
589
|
else:
|
|
546
590
|
path_ = fedit.file_path
|
|
547
591
|
|
|
548
|
-
if not
|
|
549
|
-
|
|
592
|
+
if not IS_IN_DOCKER:
|
|
593
|
+
if not os.path.exists(path_):
|
|
594
|
+
raise Exception(f"Error: file {path_} does not exist")
|
|
550
595
|
|
|
551
|
-
|
|
552
|
-
|
|
596
|
+
with open(path_) as f:
|
|
597
|
+
apply_diff_to = f.read()
|
|
598
|
+
else:
|
|
599
|
+
# Copy from docker
|
|
600
|
+
with TemporaryDirectory() as tmpdir:
|
|
601
|
+
rcode = os.system(f"docker cp {IS_IN_DOCKER}:{path_} {tmpdir}")
|
|
602
|
+
if rcode != 0:
|
|
603
|
+
raise Exception(f"Error: Read failed with code {rcode}")
|
|
604
|
+
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
605
|
+
with open(path_tmp, "r") as f:
|
|
606
|
+
apply_diff_to = f.read()
|
|
553
607
|
|
|
554
608
|
fedit.file_edit_using_search_replace_blocks = (
|
|
555
609
|
fedit.file_edit_using_search_replace_blocks.strip()
|
|
@@ -597,40 +651,20 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
597
651
|
"Error: no valid search-replace blocks found, please check your syntax for FileEdit"
|
|
598
652
|
)
|
|
599
653
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
return "Success"
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
def file_edit(fedit: FileEditFindReplace) -> str:
|
|
607
|
-
if not os.path.isabs(fedit.file_path):
|
|
608
|
-
raise Exception(
|
|
609
|
-
f"Failure: file_path should be absolute path, current working directory is {CWD}"
|
|
610
|
-
)
|
|
654
|
+
if not IS_IN_DOCKER:
|
|
655
|
+
with open(path_, "w") as f:
|
|
656
|
+
f.write(apply_diff_to)
|
|
611
657
|
else:
|
|
612
|
-
|
|
658
|
+
with TemporaryDirectory() as tmpdir:
|
|
659
|
+
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
660
|
+
with open(path_tmp, "w") as f:
|
|
661
|
+
f.write(apply_diff_to)
|
|
662
|
+
os.chmod(path_tmp, 0o777)
|
|
663
|
+
# Copy to docker using docker cp
|
|
664
|
+
rcode = os.system(f"docker cp {path_tmp} {IS_IN_DOCKER}:{path_}")
|
|
665
|
+
if rcode != 0:
|
|
666
|
+
raise Exception(f"Error: Write failed with code {rcode}")
|
|
613
667
|
|
|
614
|
-
if not os.path.exists(path_):
|
|
615
|
-
raise Exception(f"Error: file {path_} does not exist")
|
|
616
|
-
|
|
617
|
-
if not fedit.find_lines:
|
|
618
|
-
raise Exception("Error: `find_lines` cannot be empty")
|
|
619
|
-
|
|
620
|
-
out_string = "\n".join("> " + line for line in fedit.find_lines.split("\n"))
|
|
621
|
-
in_string = "\n".join("< " + line for line in fedit.replace_with_lines.split("\n"))
|
|
622
|
-
console.log(f"Editing file: {path_}\n---\n{out_string}\n---\n{in_string}\n---")
|
|
623
|
-
try:
|
|
624
|
-
with open(path_) as f:
|
|
625
|
-
content = f.read()
|
|
626
|
-
|
|
627
|
-
content = edit_content(content, fedit.find_lines, fedit.replace_with_lines)
|
|
628
|
-
|
|
629
|
-
with open(path_, "w") as f:
|
|
630
|
-
f.write(content)
|
|
631
|
-
except OSError as e:
|
|
632
|
-
raise Exception(f"Error: {e}")
|
|
633
|
-
console.print(f"File written to {path_}")
|
|
634
668
|
return "Success"
|
|
635
669
|
|
|
636
670
|
|
|
@@ -669,6 +703,10 @@ TOOLS = (
|
|
|
669
703
|
| ReadImage
|
|
670
704
|
| ReadFile
|
|
671
705
|
| Initialize
|
|
706
|
+
| Mouse
|
|
707
|
+
| Keyboard
|
|
708
|
+
| ScreenShot
|
|
709
|
+
| GetScreenInfo
|
|
672
710
|
)
|
|
673
711
|
|
|
674
712
|
|
|
@@ -702,6 +740,14 @@ def which_tool_name(name: str) -> Type[TOOLS]:
|
|
|
702
740
|
return ReadFile
|
|
703
741
|
elif name == "Initialize":
|
|
704
742
|
return Initialize
|
|
743
|
+
elif name == "Mouse":
|
|
744
|
+
return Mouse
|
|
745
|
+
elif name == "Keyboard":
|
|
746
|
+
return Keyboard
|
|
747
|
+
elif name == "ScreenShot":
|
|
748
|
+
return ScreenShot
|
|
749
|
+
elif name == "GetScreenInfo":
|
|
750
|
+
return GetScreenInfo
|
|
705
751
|
else:
|
|
706
752
|
raise ValueError(f"Unknown tool name: {name}")
|
|
707
753
|
|
|
@@ -719,12 +765,17 @@ def get_tool_output(
|
|
|
719
765
|
| DoneFlag
|
|
720
766
|
| ReadImage
|
|
721
767
|
| Initialize
|
|
722
|
-
| ReadFile
|
|
768
|
+
| ReadFile
|
|
769
|
+
| Mouse
|
|
770
|
+
| Keyboard
|
|
771
|
+
| ScreenShot
|
|
772
|
+
| GetScreenInfo,
|
|
723
773
|
enc: tiktoken.Encoding,
|
|
724
774
|
limit: float,
|
|
725
775
|
loop_call: Callable[[str, float], tuple[str, float]],
|
|
726
776
|
max_tokens: Optional[int],
|
|
727
|
-
) -> tuple[str | ImageData | DoneFlag, float]:
|
|
777
|
+
) -> tuple[list[str | ImageData | DoneFlag], float]:
|
|
778
|
+
global IS_IN_DOCKER
|
|
728
779
|
if isinstance(args, dict):
|
|
729
780
|
adapter = TypeAdapter[
|
|
730
781
|
Confirmation
|
|
@@ -739,6 +790,10 @@ def get_tool_output(
|
|
|
739
790
|
| ReadImage
|
|
740
791
|
| ReadFile
|
|
741
792
|
| Initialize
|
|
793
|
+
| Mouse
|
|
794
|
+
| Keyboard
|
|
795
|
+
| ScreenShot
|
|
796
|
+
| GetScreenInfo,
|
|
742
797
|
](
|
|
743
798
|
Confirmation
|
|
744
799
|
| BashCommand
|
|
@@ -752,6 +807,10 @@ def get_tool_output(
|
|
|
752
807
|
| ReadImage
|
|
753
808
|
| ReadFile
|
|
754
809
|
| Initialize
|
|
810
|
+
| Mouse
|
|
811
|
+
| Keyboard
|
|
812
|
+
| ScreenShot
|
|
813
|
+
| GetScreenInfo
|
|
755
814
|
)
|
|
756
815
|
arg = adapter.validate_python(args)
|
|
757
816
|
else:
|
|
@@ -762,13 +821,10 @@ def get_tool_output(
|
|
|
762
821
|
output = ask_confirmation(arg), 0.0
|
|
763
822
|
elif isinstance(arg, (BashCommand | BashInteraction)):
|
|
764
823
|
console.print("Calling execute bash tool")
|
|
765
|
-
output = execute_bash(enc, arg, max_tokens)
|
|
824
|
+
output = execute_bash(enc, arg, max_tokens, None)
|
|
766
825
|
elif isinstance(arg, CreateFileNew):
|
|
767
826
|
console.print("Calling write file tool")
|
|
768
827
|
output = write_file(arg, True), 0
|
|
769
|
-
elif isinstance(arg, FileEditFindReplace):
|
|
770
|
-
console.print("Calling file edit tool")
|
|
771
|
-
output = file_edit(arg), 0.0
|
|
772
828
|
elif isinstance(arg, FileEdit):
|
|
773
829
|
console.print("Calling full file edit tool")
|
|
774
830
|
output = do_diff_edit(arg), 0.0
|
|
@@ -790,11 +846,50 @@ def get_tool_output(
|
|
|
790
846
|
elif isinstance(arg, Initialize):
|
|
791
847
|
console.print("Calling initial info tool")
|
|
792
848
|
output = initial_info(), 0.0
|
|
849
|
+
elif isinstance(arg, (Mouse, Keyboard, ScreenShot, GetScreenInfo)):
|
|
850
|
+
console.print(f"Calling {type(arg).__name__} tool")
|
|
851
|
+
outputs_cost = run_computer_tool(arg), 0.0
|
|
852
|
+
console.print(outputs_cost[0][0])
|
|
853
|
+
outputs: list[ImageData | str | DoneFlag] = [outputs_cost[0][0]]
|
|
854
|
+
imgBs64 = outputs_cost[0][1]
|
|
855
|
+
if imgBs64:
|
|
856
|
+
console.print("Captured screenshot")
|
|
857
|
+
outputs.append(ImageData(media_type="image/png", data=imgBs64))
|
|
858
|
+
if not IS_IN_DOCKER and isinstance(arg, GetScreenInfo):
|
|
859
|
+
try:
|
|
860
|
+
# At this point we should go into the docker env
|
|
861
|
+
res, _ = execute_bash(
|
|
862
|
+
enc,
|
|
863
|
+
BashCommand(
|
|
864
|
+
command=f"docker exec -it {arg.docker_image_id} sh"
|
|
865
|
+
),
|
|
866
|
+
None,
|
|
867
|
+
0.2,
|
|
868
|
+
)
|
|
869
|
+
# At this point we should go into the docker env
|
|
870
|
+
res, _ = execute_bash(
|
|
871
|
+
enc,
|
|
872
|
+
BashInteraction(
|
|
873
|
+
send_text=f"export PS1={PROMPT}", type="BashInteraction"
|
|
874
|
+
),
|
|
875
|
+
None,
|
|
876
|
+
0.2,
|
|
877
|
+
)
|
|
878
|
+
# Do chown of home dir
|
|
879
|
+
except Exception as e:
|
|
880
|
+
reset_shell()
|
|
881
|
+
raise Exception(
|
|
882
|
+
f"Some error happened while going inside docker. I've reset the shell. Please start again. Error {e}"
|
|
883
|
+
)
|
|
884
|
+
IS_IN_DOCKER = arg.docker_image_id
|
|
885
|
+
return outputs, outputs_cost[1]
|
|
793
886
|
else:
|
|
794
887
|
raise ValueError(f"Unknown tool: {arg}")
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
888
|
+
if isinstance(output[0], str):
|
|
889
|
+
console.print(str(output[0]))
|
|
890
|
+
else:
|
|
891
|
+
console.print(f"Received {type(output[0])} from tool")
|
|
892
|
+
return [output[0]], output[1]
|
|
798
893
|
|
|
799
894
|
|
|
800
895
|
History = list[ChatCompletionMessageParam]
|
|
@@ -843,9 +938,10 @@ def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
|
843
938
|
if isinstance(mdata.data, str):
|
|
844
939
|
raise Exception(mdata)
|
|
845
940
|
try:
|
|
846
|
-
|
|
941
|
+
outputs, cost = get_tool_output(
|
|
847
942
|
mdata.data, default_enc, 0.0, lambda x, y: ("", 0), 8000
|
|
848
943
|
)
|
|
944
|
+
output = outputs[0]
|
|
849
945
|
curr_cost += cost
|
|
850
946
|
print(f"{curr_cost=}")
|
|
851
947
|
except Exception as e:
|
|
@@ -883,12 +979,22 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
|
|
|
883
979
|
if not os.path.isabs(readfile.file_path):
|
|
884
980
|
return f"Failure: file_path should be absolute path, current working directory is {CWD}"
|
|
885
981
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
982
|
+
if not IS_IN_DOCKER:
|
|
983
|
+
path = Path(readfile.file_path)
|
|
984
|
+
if not path.exists():
|
|
985
|
+
return f"Error: file {readfile.file_path} does not exist"
|
|
889
986
|
|
|
890
|
-
|
|
891
|
-
|
|
987
|
+
with path.open("r") as f:
|
|
988
|
+
content = f.read()
|
|
989
|
+
|
|
990
|
+
else:
|
|
991
|
+
return_code, content, stderr = command_run(
|
|
992
|
+
f"cat {readfile.file_path}", timeout=TIMEOUT
|
|
993
|
+
)
|
|
994
|
+
if return_code != 0:
|
|
995
|
+
raise Exception(
|
|
996
|
+
f"Error: cat {readfile.file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
|
|
997
|
+
)
|
|
892
998
|
|
|
893
999
|
if max_tokens is not None:
|
|
894
1000
|
tokens = default_enc.encode(content)
|
wcgw/types_.py
CHANGED
|
@@ -51,3 +51,44 @@ class FileEdit(BaseModel):
|
|
|
51
51
|
|
|
52
52
|
class Initialize(BaseModel):
|
|
53
53
|
type: Literal["Initialize"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GetScreenInfo(BaseModel):
|
|
57
|
+
type: Literal["GetScreenInfo"]
|
|
58
|
+
docker_image_id: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ScreenShot(BaseModel):
|
|
62
|
+
type: Literal["ScreenShot"]
|
|
63
|
+
docker_image_id: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class MouseMove(BaseModel):
|
|
67
|
+
x: int
|
|
68
|
+
y: int
|
|
69
|
+
type: Literal["MouseMove"]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class LeftClickDrag(BaseModel):
|
|
73
|
+
x: int
|
|
74
|
+
y: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MouseButton(BaseModel):
|
|
78
|
+
button_type: Literal[
|
|
79
|
+
"left_click",
|
|
80
|
+
"right_click",
|
|
81
|
+
"middle_click",
|
|
82
|
+
"double_click",
|
|
83
|
+
"scroll_up",
|
|
84
|
+
"scroll_down",
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Mouse(BaseModel):
|
|
89
|
+
action: MouseButton | LeftClickDrag | MouseMove
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class Keyboard(BaseModel):
|
|
93
|
+
action: Literal["key", "type"]
|
|
94
|
+
text: str
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: What could go wrong giving full shell access to chatgpt?
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
7
|
-
Requires-Python: <3.13,>=3.
|
|
7
|
+
Requires-Python: <3.13,>=3.11
|
|
8
8
|
Requires-Dist: anthropic>=0.39.0
|
|
9
9
|
Requires-Dist: fastapi>=0.115.0
|
|
10
|
-
Requires-Dist: mcp
|
|
10
|
+
Requires-Dist: mcp
|
|
11
11
|
Requires-Dist: mypy>=1.11.2
|
|
12
12
|
Requires-Dist: nltk>=3.9.1
|
|
13
13
|
Requires-Dist: openai>=1.46.0
|
|
@@ -27,82 +27,124 @@ Requires-Dist: uvicorn>=0.31.0
|
|
|
27
27
|
Requires-Dist: websockets>=13.1
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
|
|
30
|
+
# Shell and Coding agent on Chatgpt and Claude desktop apps
|
|
31
|
+
|
|
32
|
+
A custom gpt on chatgpt web/desktop apps to interact with your local shell, edit files, run code, etc.
|
|
32
33
|
|
|
33
34
|
[](https://github.com/rusiaaman/wcgw/actions/workflows/python-tests.yml)
|
|
34
35
|
[](https://github.com/rusiaaman/wcgw/actions/workflows/python-publish.yml)
|
|
35
36
|
|
|
37
|
+
[New feature] [26-Nov-2024] Claude desktop support for shell, computer-control, coding agent.
|
|
38
|
+
[src/wcgw/client/mcp_server/Readme.md](src/wcgw/client/mcp_server/Readme.md)
|
|
39
|
+
|
|
36
40
|
### 🚀 Highlights
|
|
41
|
+
|
|
37
42
|
- ⚡ **Full Shell Access**: No restrictions, complete control.
|
|
38
43
|
- ⚡ **Create, Execute, Iterate**: Ask the gpt to keep running compiler checks till all errors are fixed, or ask it to keep checking for the status of a long running command till it's done.
|
|
39
|
-
- ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
|
|
44
|
+
- ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
|
|
40
45
|
- ⚡ **REPL support**: [beta] Supports python/node and other REPL execution.
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
## Claude
|
|
48
|
+
Full readme [src/wcgw/client/mcp_server/Readme.md](src/wcgw/client/mcp_server/Readme.md)
|
|
49
|
+
### Setup
|
|
50
|
+
|
|
51
|
+
Update `claude_desktop_config.json`
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"wcgw": {
|
|
57
|
+
"command": "uvx",
|
|
58
|
+
"args": ["--from", "wcgw@latest", "wcgw_mcp"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then restart claude app.
|
|
65
|
+
You can then ask claude to execute shell commands, read files, edit files, run your code, etc.
|
|
66
|
+
|
|
67
|
+
## ChatGPT
|
|
68
|
+
|
|
69
|
+
### 🪜 Steps:
|
|
70
|
+
|
|
43
71
|
1. Run the [cli client](https://github.com/rusiaaman/wcgw?tab=readme-ov-file#client) in any directory of choice.
|
|
44
72
|
2. Share the generated id with this GPT: `https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access`
|
|
45
73
|
3. The custom GPT can now run any command on your cli
|
|
46
74
|
|
|
75
|
+
### Client
|
|
47
76
|
|
|
48
|
-
## Client
|
|
49
77
|
You need to keep running this client for GPT to access your shell. Run it in a version controlled project's root.
|
|
50
78
|
|
|
51
|
-
|
|
79
|
+
#### Option 1: using uv [Recommended]
|
|
80
|
+
|
|
52
81
|
```sh
|
|
53
82
|
$ curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
54
83
|
$ uvx wcgw@latest
|
|
55
84
|
```
|
|
56
85
|
|
|
57
|
-
|
|
86
|
+
#### Option 2: using pip
|
|
87
|
+
|
|
58
88
|
Supports python >=3.10 and <3.13
|
|
89
|
+
|
|
59
90
|
```sh
|
|
60
91
|
$ pip3 install wcgw
|
|
61
92
|
$ wcgw
|
|
62
93
|
```
|
|
63
94
|
|
|
64
|
-
|
|
65
95
|
This will print a UUID that you need to share with the gpt.
|
|
66
96
|
|
|
97
|
+
### Chat
|
|
67
98
|
|
|
68
|
-
## Chat
|
|
69
99
|
Open the following link or search the "wcgw" custom gpt using "Explore GPTs" on chatgpt.com
|
|
70
100
|
|
|
71
101
|
https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access
|
|
72
102
|
|
|
73
103
|
Finally, let the chatgpt know your user id in any format. E.g., "user_id=<your uuid>" followed by rest of your instructions.
|
|
74
104
|
|
|
75
|
-
NOTE: you can resume a broken connection
|
|
105
|
+
NOTE: you can resume a broken connection
|
|
76
106
|
`wcgw --client-uuid $previous_uuid`
|
|
77
107
|
|
|
78
|
-
|
|
108
|
+
### How it works on chatgpt app?
|
|
109
|
+
|
|
79
110
|
Your commands are relayed through a server to the terminal client. [You could host the server on your own](https://github.com/rusiaaman/wcgw?tab=readme-ov-file#creating-your-own-custom-gpt-and-the-relay-server). For public convenience I've hosted one at https://wcgw.arcfu.com thanks to the gcloud free tier plan.
|
|
80
111
|
|
|
81
112
|
Chatgpt sends a request to the relay server using the user id that you share with it. The relay server holds a websocket with the terminal client against the user id and acts as a proxy to pass the request.
|
|
82
113
|
|
|
83
|
-
It's secure in both the directions. Either a malicious actor or a malicious Chatgpt has to correctly guess your UUID for any security breach.
|
|
114
|
+
It's secure in both the directions. Either a malicious actor or a malicious Chatgpt has to correctly guess your UUID for any security breach.
|
|
84
115
|
|
|
85
116
|
# Showcase
|
|
86
117
|
|
|
87
|
-
##
|
|
118
|
+
## Claude desktop
|
|
119
|
+
|
|
120
|
+
### Resize image and move it to a new dir
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
## Chatgpt app
|
|
125
|
+
|
|
126
|
+
### Unit tests and github actions
|
|
127
|
+
|
|
88
128
|
[The first version of unit tests and github workflow to test on multiple python versions were written by the custom chatgpt](https://chatgpt.com/share/6717f922-8998-8005-b825-45d4b348b4dd)
|
|
89
129
|
|
|
90
|
-
|
|
91
|
-

|
|
130
|
+
### Create a todo app using react + typescript + vite
|
|
92
131
|
|
|
132
|
+

|
|
93
133
|
|
|
94
134
|
# Privacy
|
|
135
|
+
|
|
95
136
|
The relay server doesn't store any data. I can't access any information passing through it and only secure channels are used to communicate.
|
|
96
137
|
|
|
97
138
|
You may host the server on your own and create a custom gpt using the following section.
|
|
98
139
|
|
|
99
140
|
# Creating your own custom gpt and the relay server.
|
|
141
|
+
|
|
100
142
|
I've used the following instructions and action json schema to create the custom GPT. (Replace wcgw.arcfu.com with the address to your server)
|
|
101
143
|
|
|
102
144
|
https://github.com/rusiaaman/wcgw/blob/main/gpt_instructions.txt
|
|
103
145
|
https://github.com/rusiaaman/wcgw/blob/main/gpt_action_json_schema.json
|
|
104
146
|
|
|
105
|
-
Run the server
|
|
147
|
+
Run the server
|
|
106
148
|
`gunicorn --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:443 src.wcgw.relay.serve:app --certfile fullchain.pem --keyfile privkey.pem`
|
|
107
149
|
|
|
108
150
|
If you don't have public ip and domain name, you can use `ngrok` or similar services to get a https address to the api.
|
|
@@ -110,19 +152,24 @@ If you don't have public ip and domain name, you can use `ngrok` or similar serv
|
|
|
110
152
|
The specify the server url in the `wcgw` command like so
|
|
111
153
|
`wcgw --server-url https://your-url/v1/register`
|
|
112
154
|
|
|
113
|
-
#
|
|
114
|
-
WCGW now supports Claude Desktop through the MCP protocol, allowing you to use Claude's capabilities directly from your desktop environment. This integration enables seamless interaction between Claude and your local shell.
|
|
155
|
+
# [Optional] Local shell access with openai API key or anthropic API key
|
|
115
156
|
|
|
116
|
-
|
|
157
|
+
## Openai
|
|
117
158
|
|
|
118
159
|
Add `OPENAI_API_KEY` and `OPENAI_ORG_ID` env variables.
|
|
119
160
|
|
|
120
|
-
|
|
161
|
+
Then run
|
|
162
|
+
|
|
163
|
+
`uvx --from wcgw@latest wcgw_local --limit 0.1` # Cost limit $0.1
|
|
164
|
+
|
|
165
|
+
You can now directly write messages or press enter key to open vim for multiline message and text pasting.
|
|
166
|
+
|
|
167
|
+
## Anthropic
|
|
121
168
|
|
|
122
|
-
`
|
|
169
|
+
Add `ANTHROPIC_API_KEY` env variable.
|
|
123
170
|
|
|
124
|
-
Then run
|
|
171
|
+
Then run
|
|
125
172
|
|
|
126
|
-
`
|
|
173
|
+
`uvx --from wcgw@latest wcgw_local --claude`
|
|
127
174
|
|
|
128
175
|
You can now directly write messages or press enter key to open vim for multiline message and text pasting.
|