wcgw 1.0.0__py3-none-any.whl → 1.1.0__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/__init__.py +1 -1
- wcgw/client/anthropic_client.py +415 -0
- wcgw/client/cli.py +40 -0
- wcgw/client/diff-instructions.txt +44 -0
- wcgw/client/{basic.py → openai_client.py} +41 -13
- wcgw/client/tools.py +223 -105
- wcgw/relay/serve.py +22 -71
- wcgw/types_.py +26 -0
- {wcgw-1.0.0.dist-info → wcgw-1.1.0.dist-info}/METADATA +3 -2
- wcgw-1.1.0.dist-info/RECORD +17 -0
- {wcgw-1.0.0.dist-info → wcgw-1.1.0.dist-info}/WHEEL +1 -1
- wcgw/client/claude.py +0 -384
- wcgw-1.0.0.dist-info/RECORD +0 -15
- /wcgw/client/{openai_adapters.py → __init__.py} +0 -0
- {wcgw-1.0.0.dist-info → wcgw-1.1.0.dist-info}/entry_points.txt +0 -0
wcgw/client/tools.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
from io import BytesIO
|
|
3
5
|
import json
|
|
4
6
|
import mimetypes
|
|
7
|
+
from pathlib import Path
|
|
5
8
|
import re
|
|
6
9
|
import sys
|
|
7
10
|
import threading
|
|
@@ -13,6 +16,7 @@ from typing import (
|
|
|
13
16
|
NewType,
|
|
14
17
|
Optional,
|
|
15
18
|
ParamSpec,
|
|
19
|
+
Type,
|
|
16
20
|
TypeVar,
|
|
17
21
|
TypedDict,
|
|
18
22
|
)
|
|
@@ -41,7 +45,14 @@ from openai.types.chat import (
|
|
|
41
45
|
ParsedChatCompletionMessage,
|
|
42
46
|
)
|
|
43
47
|
from nltk.metrics.distance import edit_distance
|
|
44
|
-
|
|
48
|
+
|
|
49
|
+
from ..types_ import (
|
|
50
|
+
CreateFileNew,
|
|
51
|
+
FileEditFindReplace,
|
|
52
|
+
FullFileEdit,
|
|
53
|
+
ResetShell,
|
|
54
|
+
Writefile,
|
|
55
|
+
)
|
|
45
56
|
|
|
46
57
|
from ..types_ import BashCommand
|
|
47
58
|
|
|
@@ -70,7 +81,10 @@ def render_terminal_output(text: str) -> str:
|
|
|
70
81
|
break
|
|
71
82
|
else:
|
|
72
83
|
i = len(dsp)
|
|
73
|
-
|
|
84
|
+
lines = screen.display[: len(dsp) - i]
|
|
85
|
+
# Strip trailing space
|
|
86
|
+
lines = [line.rstrip() for line in lines]
|
|
87
|
+
return "\n".join(lines)
|
|
74
88
|
|
|
75
89
|
|
|
76
90
|
class Confirmation(BaseModel):
|
|
@@ -368,8 +382,16 @@ def serve_image_in_bg(file_path: str, client_uuid: str, name: str) -> None:
|
|
|
368
382
|
serve_image_in_bg(file_path, client_uuid, name)
|
|
369
383
|
|
|
370
384
|
|
|
385
|
+
MEDIA_TYPES = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
386
|
+
|
|
387
|
+
|
|
371
388
|
class ImageData(BaseModel):
|
|
372
|
-
|
|
389
|
+
media_type: MEDIA_TYPES
|
|
390
|
+
data: str
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def dataurl(self) -> str:
|
|
394
|
+
return f"data:{self.media_type};base64," + self.data
|
|
373
395
|
|
|
374
396
|
|
|
375
397
|
Param = ParamSpec("Param")
|
|
@@ -399,16 +421,25 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
399
421
|
image_bytes = image_file.read()
|
|
400
422
|
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
|
401
423
|
image_type = mimetypes.guess_type(file_path)[0]
|
|
402
|
-
return ImageData(
|
|
424
|
+
return ImageData(media_type=image_type, data=image_b64)
|
|
403
425
|
|
|
404
426
|
|
|
405
|
-
def write_file(writefile: Writefile) -> str:
|
|
427
|
+
def write_file(writefile: Writefile | CreateFileNew, error_on_exist: bool) -> str:
|
|
406
428
|
if not os.path.isabs(writefile.file_path):
|
|
407
|
-
|
|
429
|
+
return "Failure: file_path should be absolute path"
|
|
408
430
|
else:
|
|
409
431
|
path_ = writefile.file_path
|
|
432
|
+
|
|
433
|
+
if error_on_exist and os.path.exists(path_):
|
|
434
|
+
file_data = Path(path_).read_text()
|
|
435
|
+
if file_data:
|
|
436
|
+
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
437
|
+
|
|
438
|
+
path = Path(path_)
|
|
439
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
440
|
+
|
|
410
441
|
try:
|
|
411
|
-
with open(
|
|
442
|
+
with path.open("w") as f:
|
|
412
443
|
f.write(writefile.file_content)
|
|
413
444
|
except OSError as e:
|
|
414
445
|
return f"Error: {e}"
|
|
@@ -416,50 +447,128 @@ def write_file(writefile: Writefile) -> str:
|
|
|
416
447
|
return "Success"
|
|
417
448
|
|
|
418
449
|
|
|
419
|
-
def find_least_edit_distance_substring(
|
|
420
|
-
|
|
450
|
+
def find_least_edit_distance_substring(
|
|
451
|
+
content: str, find_str: str
|
|
452
|
+
) -> tuple[str, float]:
|
|
453
|
+
orig_content_lines = content.split("\n")
|
|
454
|
+
content_lines = [
|
|
455
|
+
line.strip() for line in orig_content_lines
|
|
456
|
+
] # Remove trailing and leading space for calculating edit distance
|
|
421
457
|
find_lines = find_str.split("\n")
|
|
458
|
+
find_lines = [
|
|
459
|
+
line.strip() for line in find_lines
|
|
460
|
+
] # Remove trailing and leading space for calculating edit distance
|
|
422
461
|
# Slide window and find one with sum of edit distance least
|
|
423
462
|
min_edit_distance = float("inf")
|
|
424
463
|
min_edit_distance_lines = []
|
|
425
|
-
for i in range(len(content_lines) - len(find_lines) + 1):
|
|
464
|
+
for i in range(max(1, len(content_lines) - len(find_lines) + 1)):
|
|
426
465
|
edit_distance_sum = 0
|
|
427
466
|
for j in range(len(find_lines)):
|
|
428
|
-
|
|
467
|
+
if (i + j) < len(content_lines):
|
|
468
|
+
edit_distance_sum += edit_distance(content_lines[i + j], find_lines[j])
|
|
469
|
+
else:
|
|
470
|
+
edit_distance_sum += len(find_lines[j])
|
|
429
471
|
if edit_distance_sum < min_edit_distance:
|
|
430
472
|
min_edit_distance = edit_distance_sum
|
|
431
|
-
min_edit_distance_lines =
|
|
432
|
-
return "\n".join(min_edit_distance_lines)
|
|
473
|
+
min_edit_distance_lines = orig_content_lines[i : i + len(find_lines)]
|
|
474
|
+
return "\n".join(min_edit_distance_lines), min_edit_distance
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str:
|
|
478
|
+
count = content.count(find_lines)
|
|
479
|
+
if count == 0:
|
|
480
|
+
closest_match, min_edit_distance = find_least_edit_distance_substring(
|
|
481
|
+
content, find_lines
|
|
482
|
+
)
|
|
483
|
+
print(
|
|
484
|
+
f"Exact match not found, found with whitespace removed edit distance: {min_edit_distance}"
|
|
485
|
+
)
|
|
486
|
+
if min_edit_distance / len(find_lines) < 1 / 100:
|
|
487
|
+
print("Editing file with closest match")
|
|
488
|
+
return edit_content(content, closest_match, replace_with_lines)
|
|
489
|
+
raise Exception(
|
|
490
|
+
f"Error: no match found for the provided `find_lines` in the file. Closest match:\n---\n{closest_match}\n---\nFile not edited"
|
|
491
|
+
)
|
|
433
492
|
|
|
493
|
+
content = content.replace(find_lines, replace_with_lines, 1)
|
|
494
|
+
return content
|
|
434
495
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
496
|
+
|
|
497
|
+
def do_diff_edit(fedit: FullFileEdit) -> str:
|
|
498
|
+
console.log(f"Editing file: {fedit.file_path}")
|
|
499
|
+
|
|
500
|
+
if not os.path.isabs(fedit.file_path):
|
|
501
|
+
raise Exception("Failure: file_path should be absolute path")
|
|
438
502
|
else:
|
|
439
|
-
path_ =
|
|
503
|
+
path_ = fedit.file_path
|
|
504
|
+
|
|
505
|
+
if not os.path.exists(path_):
|
|
506
|
+
raise Exception(f"Error: file {path_} does not exist")
|
|
507
|
+
|
|
508
|
+
with open(path_) as f:
|
|
509
|
+
apply_diff_to = f.read()
|
|
510
|
+
|
|
511
|
+
lines = fedit.file_edit_using_searh_replace_blocks.split("\n")
|
|
512
|
+
n_lines = len(lines)
|
|
513
|
+
i = 0
|
|
514
|
+
while i < n_lines:
|
|
515
|
+
if re.match(r"^<<<<<<+\s*SEARCH\s*$", lines[i]):
|
|
516
|
+
search_block = []
|
|
517
|
+
i += 1
|
|
518
|
+
while i < n_lines and not re.match(r"^======*\s*$", lines[i]):
|
|
519
|
+
search_block.append(lines[i])
|
|
520
|
+
i += 1
|
|
521
|
+
i += 1
|
|
522
|
+
replace_block = []
|
|
523
|
+
while i < n_lines and not re.match(r"^>>>>>>+\s*REPLACE\s*$", lines[i]):
|
|
524
|
+
replace_block.append(lines[i])
|
|
525
|
+
i += 1
|
|
526
|
+
i += 1
|
|
527
|
+
|
|
528
|
+
for line in search_block:
|
|
529
|
+
console.log("> " + line)
|
|
530
|
+
console.log("---")
|
|
531
|
+
for line in replace_block:
|
|
532
|
+
console.log("< " + line)
|
|
533
|
+
|
|
534
|
+
search_block_ = "\n".join(search_block)
|
|
535
|
+
replace_block_ = "\n".join(replace_block)
|
|
536
|
+
|
|
537
|
+
apply_diff_to = edit_content(apply_diff_to, search_block_, replace_block_)
|
|
538
|
+
else:
|
|
539
|
+
i += 1
|
|
440
540
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
541
|
+
with open(path_, "w") as f:
|
|
542
|
+
f.write(apply_diff_to)
|
|
543
|
+
|
|
544
|
+
return "Success"
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def file_edit(fedit: FileEditFindReplace) -> str:
|
|
548
|
+
if not os.path.isabs(fedit.file_path):
|
|
549
|
+
raise Exception("Failure: file_path should be absolute path")
|
|
550
|
+
else:
|
|
551
|
+
path_ = fedit.file_path
|
|
552
|
+
|
|
553
|
+
if not os.path.exists(path_):
|
|
554
|
+
raise Exception(f"Error: file {path_} does not exist")
|
|
555
|
+
|
|
556
|
+
if not fedit.find_lines:
|
|
557
|
+
raise Exception("Error: `find_lines` cannot be empty")
|
|
558
|
+
|
|
559
|
+
out_string = "\n".join("> " + line for line in fedit.find_lines.split("\n"))
|
|
560
|
+
in_string = "\n".join("< " + line for line in fedit.replace_with_lines.split("\n"))
|
|
445
561
|
console.log(f"Editing file: {path_}\n---\n{out_string}\n---\n{in_string}\n---")
|
|
446
562
|
try:
|
|
447
563
|
with open(path_) as f:
|
|
448
564
|
content = f.read()
|
|
449
|
-
# First find counts
|
|
450
|
-
count = content.count(file_edit.find_lines)
|
|
451
565
|
|
|
452
|
-
|
|
453
|
-
closest_match = find_least_edit_distance_substring(
|
|
454
|
-
content, file_edit.find_lines
|
|
455
|
-
)
|
|
456
|
-
return f"Error: no match found for the provided `find_lines` in the file. Closest match:\n---\n{closest_match}\n---\nFile not edited"
|
|
566
|
+
content = edit_content(content, fedit.find_lines, fedit.replace_with_lines)
|
|
457
567
|
|
|
458
|
-
content = content.replace(file_edit.find_lines, file_edit.replace_with_lines)
|
|
459
568
|
with open(path_, "w") as f:
|
|
460
569
|
f.write(content)
|
|
461
570
|
except OSError as e:
|
|
462
|
-
|
|
571
|
+
raise Exception(f"Error: {e}")
|
|
463
572
|
console.print(f"File written to {path_}")
|
|
464
573
|
return "Success"
|
|
465
574
|
|
|
@@ -486,31 +595,53 @@ def take_help_of_ai_assistant(
|
|
|
486
595
|
return output, cost
|
|
487
596
|
|
|
488
597
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
| FileEditFindReplace
|
|
507
|
-
| AIAssistant
|
|
508
|
-
| DoneFlag
|
|
509
|
-
| ReadImage
|
|
510
|
-
)
|
|
598
|
+
TOOLS = (
|
|
599
|
+
Confirmation
|
|
600
|
+
| BashCommand
|
|
601
|
+
| BashInteraction
|
|
602
|
+
| ResetShell
|
|
603
|
+
| Writefile
|
|
604
|
+
| CreateFileNew
|
|
605
|
+
| FileEditFindReplace
|
|
606
|
+
| FullFileEdit
|
|
607
|
+
| AIAssistant
|
|
608
|
+
| DoneFlag
|
|
609
|
+
| ReadImage
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def which_tool(args: str) -> TOOLS:
|
|
614
|
+
adapter = TypeAdapter[TOOLS](TOOLS)
|
|
511
615
|
return adapter.validate_python(json.loads(args))
|
|
512
616
|
|
|
513
617
|
|
|
618
|
+
def which_tool_name(name: str) -> Type[TOOLS]:
|
|
619
|
+
if name == "Confirmation":
|
|
620
|
+
return Confirmation
|
|
621
|
+
elif name == "BashCommand":
|
|
622
|
+
return BashCommand
|
|
623
|
+
elif name == "BashInteraction":
|
|
624
|
+
return BashInteraction
|
|
625
|
+
elif name == "ResetShell":
|
|
626
|
+
return ResetShell
|
|
627
|
+
elif name == "Writefile":
|
|
628
|
+
return Writefile
|
|
629
|
+
elif name == "CreateFileNew":
|
|
630
|
+
return CreateFileNew
|
|
631
|
+
elif name == "FileEditFindReplace":
|
|
632
|
+
return FileEditFindReplace
|
|
633
|
+
elif name == "FullFileEdit":
|
|
634
|
+
return FullFileEdit
|
|
635
|
+
elif name == "AIAssistant":
|
|
636
|
+
return AIAssistant
|
|
637
|
+
elif name == "DoneFlag":
|
|
638
|
+
return DoneFlag
|
|
639
|
+
elif name == "ReadImage":
|
|
640
|
+
return ReadImage
|
|
641
|
+
else:
|
|
642
|
+
raise ValueError(f"Unknown tool name: {name}")
|
|
643
|
+
|
|
644
|
+
|
|
514
645
|
def get_tool_output(
|
|
515
646
|
args: dict[object, object]
|
|
516
647
|
| Confirmation
|
|
@@ -518,7 +649,9 @@ def get_tool_output(
|
|
|
518
649
|
| BashInteraction
|
|
519
650
|
| ResetShell
|
|
520
651
|
| Writefile
|
|
652
|
+
| CreateFileNew
|
|
521
653
|
| FileEditFindReplace
|
|
654
|
+
| FullFileEdit
|
|
522
655
|
| AIAssistant
|
|
523
656
|
| DoneFlag
|
|
524
657
|
| ReadImage,
|
|
@@ -534,7 +667,9 @@ def get_tool_output(
|
|
|
534
667
|
| BashInteraction
|
|
535
668
|
| ResetShell
|
|
536
669
|
| Writefile
|
|
670
|
+
| CreateFileNew
|
|
537
671
|
| FileEditFindReplace
|
|
672
|
+
| FullFileEdit
|
|
538
673
|
| AIAssistant
|
|
539
674
|
| DoneFlag
|
|
540
675
|
| ReadImage
|
|
@@ -544,7 +679,9 @@ def get_tool_output(
|
|
|
544
679
|
| BashInteraction
|
|
545
680
|
| ResetShell
|
|
546
681
|
| Writefile
|
|
682
|
+
| CreateFileNew
|
|
547
683
|
| FileEditFindReplace
|
|
684
|
+
| FullFileEdit
|
|
548
685
|
| AIAssistant
|
|
549
686
|
| DoneFlag
|
|
550
687
|
| ReadImage
|
|
@@ -561,10 +698,16 @@ def get_tool_output(
|
|
|
561
698
|
output = execute_bash(enc, arg, max_tokens)
|
|
562
699
|
elif isinstance(arg, Writefile):
|
|
563
700
|
console.print("Calling write file tool")
|
|
564
|
-
output = write_file(arg), 0
|
|
701
|
+
output = write_file(arg, False), 0
|
|
702
|
+
elif isinstance(arg, CreateFileNew):
|
|
703
|
+
console.print("Calling write file tool")
|
|
704
|
+
output = write_file(arg, True), 0
|
|
565
705
|
elif isinstance(arg, FileEditFindReplace):
|
|
566
706
|
console.print("Calling file edit tool")
|
|
567
707
|
output = file_edit(arg), 0.0
|
|
708
|
+
elif isinstance(arg, FullFileEdit):
|
|
709
|
+
console.print("Calling full file edit tool")
|
|
710
|
+
output = do_diff_edit(arg), 0.0
|
|
568
711
|
elif isinstance(arg, DoneFlag):
|
|
569
712
|
console.print("Calling mark finish tool")
|
|
570
713
|
output = mark_finish(arg), 0.0
|
|
@@ -593,44 +736,29 @@ curr_cost = 0.0
|
|
|
593
736
|
|
|
594
737
|
|
|
595
738
|
class Mdata(BaseModel):
|
|
596
|
-
data:
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
user_input = input()
|
|
606
|
-
with execution_lock:
|
|
607
|
-
try:
|
|
608
|
-
console.log(
|
|
609
|
-
execute_bash(
|
|
610
|
-
default_enc,
|
|
611
|
-
BashInteraction(
|
|
612
|
-
send_ascii=[ord(x) for x in user_input] + [ord("\n")]
|
|
613
|
-
),
|
|
614
|
-
max_tokens=None,
|
|
615
|
-
)[0]
|
|
616
|
-
)
|
|
617
|
-
except Exception as e:
|
|
618
|
-
traceback.print_exc()
|
|
619
|
-
console.log(f"Error: {e}")
|
|
739
|
+
data: (
|
|
740
|
+
BashCommand
|
|
741
|
+
| BashInteraction
|
|
742
|
+
| Writefile
|
|
743
|
+
| CreateFileNew
|
|
744
|
+
| ResetShell
|
|
745
|
+
| FileEditFindReplace
|
|
746
|
+
| FullFileEdit
|
|
747
|
+
)
|
|
620
748
|
|
|
621
749
|
|
|
622
|
-
|
|
750
|
+
def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
623
751
|
global default_enc, default_model, curr_cost
|
|
624
752
|
# Generate a unique UUID for this client
|
|
625
753
|
if not client_uuid:
|
|
626
754
|
client_uuid = str(uuid.uuid4())
|
|
627
755
|
|
|
628
756
|
# Create the WebSocket connection
|
|
629
|
-
|
|
630
|
-
server_version = str(
|
|
757
|
+
with syncconnect(f"{server_url}/{client_uuid}") as websocket:
|
|
758
|
+
server_version = str(websocket.recv())
|
|
631
759
|
print(f"Server version: {server_version}")
|
|
632
760
|
client_version = importlib.metadata.version("wcgw")
|
|
633
|
-
|
|
761
|
+
websocket.send(client_version)
|
|
634
762
|
|
|
635
763
|
print(
|
|
636
764
|
f"Connected. Share this user id with the chatbot: {client_uuid} \nLink: https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access"
|
|
@@ -638,24 +766,23 @@ async def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
|
638
766
|
try:
|
|
639
767
|
while True:
|
|
640
768
|
# Wait to receive data from the server
|
|
641
|
-
message =
|
|
769
|
+
message = websocket.recv()
|
|
642
770
|
mdata = Mdata.model_validate_json(message)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
await websocket.send(output)
|
|
771
|
+
try:
|
|
772
|
+
output, cost = get_tool_output(
|
|
773
|
+
mdata.data, default_enc, 0.0, lambda x, y: ("", 0), None
|
|
774
|
+
)
|
|
775
|
+
curr_cost += cost
|
|
776
|
+
print(f"{curr_cost=}")
|
|
777
|
+
except Exception as e:
|
|
778
|
+
output = f"GOT EXCEPTION while calling tool. Error: {e}"
|
|
779
|
+
traceback.print_exc()
|
|
780
|
+
assert isinstance(output, str)
|
|
781
|
+
websocket.send(output)
|
|
655
782
|
|
|
656
783
|
except (websockets.ConnectionClosed, ConnectionError):
|
|
657
784
|
print(f"Connection closed for UUID: {client_uuid}, retrying")
|
|
658
|
-
|
|
785
|
+
register_client(server_url, client_uuid)
|
|
659
786
|
|
|
660
787
|
|
|
661
788
|
run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True)
|
|
@@ -672,13 +799,4 @@ def app(
|
|
|
672
799
|
print(f"wcgw version: {version_}")
|
|
673
800
|
exit()
|
|
674
801
|
|
|
675
|
-
|
|
676
|
-
thread2 = threading.Thread(
|
|
677
|
-
target=asyncio.run, args=(register_client(server_url, client_uuid or ""),)
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
thread1.start()
|
|
681
|
-
thread2.start()
|
|
682
|
-
|
|
683
|
-
thread1.join()
|
|
684
|
-
thread2.join()
|
|
802
|
+
register_client(server_url, client_uuid or "")
|
wcgw/relay/serve.py
CHANGED
|
@@ -17,7 +17,9 @@ from dotenv import load_dotenv
|
|
|
17
17
|
from ..types_ import (
|
|
18
18
|
BashCommand,
|
|
19
19
|
BashInteraction,
|
|
20
|
+
CreateFileNew,
|
|
20
21
|
FileEditFindReplace,
|
|
22
|
+
FullFileEdit,
|
|
21
23
|
ResetShell,
|
|
22
24
|
Writefile,
|
|
23
25
|
Specials,
|
|
@@ -25,7 +27,15 @@ from ..types_ import (
|
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
class Mdata(BaseModel):
|
|
28
|
-
data:
|
|
30
|
+
data: (
|
|
31
|
+
BashCommand
|
|
32
|
+
| BashInteraction
|
|
33
|
+
| Writefile
|
|
34
|
+
| CreateFileNew
|
|
35
|
+
| ResetShell
|
|
36
|
+
| FileEditFindReplace
|
|
37
|
+
| FullFileEdit
|
|
38
|
+
)
|
|
29
39
|
user_id: UUID
|
|
30
40
|
|
|
31
41
|
|
|
@@ -38,39 +48,6 @@ gpts: dict[UUID, Callable[[str], None]] = {}
|
|
|
38
48
|
images: DefaultDict[UUID, dict[str, dict[str, Any]]] = DefaultDict(dict)
|
|
39
49
|
|
|
40
50
|
|
|
41
|
-
@app.websocket("/register_serve_image/{uuid}")
|
|
42
|
-
async def register_serve_image(websocket: WebSocket, uuid: UUID) -> None:
|
|
43
|
-
raise Exception("Disabled")
|
|
44
|
-
await websocket.accept()
|
|
45
|
-
received_data = await websocket.receive_json()
|
|
46
|
-
name = received_data["name"]
|
|
47
|
-
image_b64 = received_data["image_b64"]
|
|
48
|
-
image_bytes = base64.b64decode(image_b64)
|
|
49
|
-
images[uuid][name] = {
|
|
50
|
-
"content": image_bytes,
|
|
51
|
-
"media_type": received_data["media_type"],
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@app.get("/get_image/{uuid}/{name}")
|
|
56
|
-
async def get_image(uuid: UUID, name: str) -> fastapi.responses.Response:
|
|
57
|
-
return fastapi.responses.Response(
|
|
58
|
-
content=images[uuid][name]["content"],
|
|
59
|
-
media_type=images[uuid][name]["media_type"],
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@app.websocket("/register/{uuid}")
|
|
64
|
-
async def register_websocket_deprecated(websocket: WebSocket, uuid: UUID) -> None:
|
|
65
|
-
await websocket.accept()
|
|
66
|
-
await websocket.send_text(
|
|
67
|
-
"Outdated client used. Deprecated api is being used. Upgrade the wcgw app."
|
|
68
|
-
)
|
|
69
|
-
await websocket.close(
|
|
70
|
-
reason="This endpoint is deprecated. Please use /v1/register/{uuid}", code=1002
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
|
|
74
51
|
CLIENT_VERSION_MINIMUM = "1.0.0"
|
|
75
52
|
|
|
76
53
|
|
|
@@ -116,25 +93,15 @@ async def register_websocket(websocket: WebSocket, uuid: UUID) -> None:
|
|
|
116
93
|
print(f"Client {uuid} disconnected")
|
|
117
94
|
|
|
118
95
|
|
|
119
|
-
|
|
120
|
-
async def write_file_deprecated(write_file_data: Writefile, user_id: UUID) -> Response:
|
|
121
|
-
return Response(
|
|
122
|
-
content="This version of the API is deprecated. Please upgrade your client.",
|
|
123
|
-
status_code=400,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
class WritefileWithUUID(Writefile):
|
|
96
|
+
class CreateFileNewWithUUID(CreateFileNew):
|
|
128
97
|
user_id: UUID
|
|
129
98
|
|
|
130
99
|
|
|
131
|
-
@app.post("/v1/
|
|
132
|
-
async def
|
|
100
|
+
@app.post("/v1/create_file")
|
|
101
|
+
async def create_file(write_file_data: CreateFileNewWithUUID) -> str:
|
|
133
102
|
user_id = write_file_data.user_id
|
|
134
103
|
if user_id not in clients:
|
|
135
|
-
|
|
136
|
-
status_code=404, detail="User with the provided id not found"
|
|
137
|
-
)
|
|
104
|
+
return "Failure: id not found, ask the user to check it."
|
|
138
105
|
|
|
139
106
|
results: Optional[str] = None
|
|
140
107
|
|
|
@@ -155,19 +122,17 @@ async def write_file(write_file_data: WritefileWithUUID) -> str:
|
|
|
155
122
|
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
156
123
|
|
|
157
124
|
|
|
158
|
-
class
|
|
125
|
+
class FullFileEditWithUUID(FullFileEdit):
|
|
159
126
|
user_id: UUID
|
|
160
127
|
|
|
161
128
|
|
|
162
|
-
@app.post("/v1/
|
|
129
|
+
@app.post("/v1/full_file_edit")
|
|
163
130
|
async def file_edit_find_replace(
|
|
164
|
-
file_edit_find_replace:
|
|
131
|
+
file_edit_find_replace: FullFileEditWithUUID,
|
|
165
132
|
) -> str:
|
|
166
133
|
user_id = file_edit_find_replace.user_id
|
|
167
134
|
if user_id not in clients:
|
|
168
|
-
|
|
169
|
-
status_code=404, detail="User with the provided id not found"
|
|
170
|
-
)
|
|
135
|
+
return "Failure: id not found, ask the user to check it."
|
|
171
136
|
|
|
172
137
|
results: Optional[str] = None
|
|
173
138
|
|
|
@@ -201,9 +166,7 @@ class ResetShellWithUUID(ResetShell):
|
|
|
201
166
|
async def reset_shell(reset_shell: ResetShellWithUUID) -> str:
|
|
202
167
|
user_id = reset_shell.user_id
|
|
203
168
|
if user_id not in clients:
|
|
204
|
-
|
|
205
|
-
status_code=404, detail="User with the provided id not found"
|
|
206
|
-
)
|
|
169
|
+
return "Failure: id not found, ask the user to check it."
|
|
207
170
|
|
|
208
171
|
results: Optional[str] = None
|
|
209
172
|
|
|
@@ -224,14 +187,6 @@ async def reset_shell(reset_shell: ResetShellWithUUID) -> str:
|
|
|
224
187
|
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
225
188
|
|
|
226
189
|
|
|
227
|
-
@app.post("/execute_bash")
|
|
228
|
-
async def execute_bash_deprecated(excute_bash_data: Any, user_id: UUID) -> Response:
|
|
229
|
-
return Response(
|
|
230
|
-
content="This version of the API is deprecated. Please upgrade your client.",
|
|
231
|
-
status_code=400,
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
|
|
235
190
|
class CommandWithUUID(BaseModel):
|
|
236
191
|
command: str
|
|
237
192
|
user_id: UUID
|
|
@@ -241,9 +196,7 @@ class CommandWithUUID(BaseModel):
|
|
|
241
196
|
async def bash_command(command: CommandWithUUID) -> str:
|
|
242
197
|
user_id = command.user_id
|
|
243
198
|
if user_id not in clients:
|
|
244
|
-
|
|
245
|
-
status_code=404, detail="User with the provided id not found"
|
|
246
|
-
)
|
|
199
|
+
return "Failure: id not found, ask the user to check it."
|
|
247
200
|
|
|
248
201
|
results: Optional[str] = None
|
|
249
202
|
|
|
@@ -274,9 +227,7 @@ class BashInteractionWithUUID(BashInteraction):
|
|
|
274
227
|
async def bash_interaction(bash_interaction: BashInteractionWithUUID) -> str:
|
|
275
228
|
user_id = bash_interaction.user_id
|
|
276
229
|
if user_id not in clients:
|
|
277
|
-
|
|
278
|
-
status_code=404, detail="User with the provided id not found"
|
|
279
|
-
)
|
|
230
|
+
return "Failure: id not found, ask the user to check it."
|
|
280
231
|
|
|
281
232
|
results: Optional[str] = None
|
|
282
233
|
|
wcgw/types_.py
CHANGED
|
@@ -16,6 +16,22 @@ class BashInteraction(BaseModel):
|
|
|
16
16
|
send_specials: Optional[Sequence[Specials]] = None
|
|
17
17
|
send_ascii: Optional[Sequence[int]] = None
|
|
18
18
|
|
|
19
|
+
def model_post_init(self, __context: object) -> None:
|
|
20
|
+
# Ensure only one of the fields is set
|
|
21
|
+
if (
|
|
22
|
+
sum(
|
|
23
|
+
[
|
|
24
|
+
int(bool(self.send_text)),
|
|
25
|
+
int(bool(self.send_specials)),
|
|
26
|
+
int(bool(self.send_ascii)),
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
!= 1
|
|
30
|
+
):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
"Exactly one of 'send_text', 'send_specials', or 'send_ascii' must be set"
|
|
33
|
+
)
|
|
34
|
+
|
|
19
35
|
|
|
20
36
|
class ReadImage(BaseModel):
|
|
21
37
|
file_path: str
|
|
@@ -27,6 +43,11 @@ class Writefile(BaseModel):
|
|
|
27
43
|
file_content: str
|
|
28
44
|
|
|
29
45
|
|
|
46
|
+
class CreateFileNew(BaseModel):
|
|
47
|
+
file_path: str
|
|
48
|
+
file_content: str
|
|
49
|
+
|
|
50
|
+
|
|
30
51
|
class FileEditFindReplace(BaseModel):
|
|
31
52
|
file_path: str
|
|
32
53
|
find_lines: str
|
|
@@ -35,3 +56,8 @@ class FileEditFindReplace(BaseModel):
|
|
|
35
56
|
|
|
36
57
|
class ResetShell(BaseModel):
|
|
37
58
|
should_reset: Literal[True] = True
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class FullFileEdit(BaseModel):
|
|
62
|
+
file_path: str
|
|
63
|
+
file_edit_using_searh_replace_blocks: str
|