wcgw 2.2.1__py3-none-any.whl → 2.3.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.

@@ -169,10 +169,11 @@ def loop(
169
169
  - Do not use interactive commands like nano. Prefer writing simpler commands.
170
170
  - Status of the command and the current working directory will always be returned at the end.
171
171
  - Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
172
- - The first line might be `(...truncated)` if the output is too long.
172
+ - The first or the last line might be `(...truncated)` if the output is too long.
173
173
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
174
174
  - The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
175
175
  - Run long running commands in background using screen instead of "&".
176
+ - Use longer wait_for_seconds if the command is expected to run for a long time.
176
177
  """,
177
178
  ),
178
179
  ToolParam(
@@ -185,8 +186,9 @@ def loop(
185
186
  - Send send_specials=["Enter"] to recheck status of a running program.
186
187
  - Only one of send_text, send_specials, send_ascii should be provided.
187
188
  - This returns within 5 seconds, for heavy programs keep checking status for upto 10 turns before asking user to continue checking again.
188
- - 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 using ["Enter"].
189
- - Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
189
+ - 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 using ["Enter"].
190
+ - Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
191
+ - Set longer wait_for_seconds when program is expected to run for a long time.
190
192
  """,
191
193
  ),
192
194
  ToolParam(
@@ -195,6 +197,7 @@ def loop(
195
197
  description="""
196
198
  - Read full file content
197
199
  - Provide absolute file path only
200
+ - Use this instead of 'cat' from BashCommand
198
201
  """,
199
202
  ),
200
203
  ToolParam(
@@ -89,10 +89,11 @@ async def handle_list_tools() -> list[types.Tool]:
89
89
  - Do not use interactive commands like nano. Prefer writing simpler commands.
90
90
  - Status of the command and the current working directory will always be returned at the end.
91
91
  - Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
92
- - The first line might be `(...truncated)` if the output is too long.
92
+ - The first or the last line might be `(...truncated)` if the output is too long.
93
93
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
94
- - The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
94
+ - The control will return to you in 3 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
95
95
  - Run long running commands in background using screen instead of "&".
96
+ - Use longer wait_for_seconds if the command is expected to run for a long time.
96
97
  """,
97
98
  ),
98
99
  ToolParam(
@@ -105,8 +106,9 @@ async def handle_list_tools() -> list[types.Tool]:
105
106
  - Send send_specials=["Enter"] to recheck status of a running program.
106
107
  - Only one of send_text, send_specials, send_ascii should be provided.
107
108
  - This returns within 3 seconds, for heavy programs keep checking status for upto 10 turns before asking user to continue checking again.
108
- - 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 using ["Enter"].
109
- - Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
109
+ - 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 using ["Enter"].
110
+ - Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
111
+ - Set longer wait_for_seconds when program is expected to run for a long time.
110
112
  """,
111
113
  ),
112
114
  ToolParam(
@@ -115,6 +117,7 @@ async def handle_list_tools() -> list[types.Tool]:
115
117
  description="""
116
118
  - Read full file content
117
119
  - Provide absolute file path only
120
+ - Use this instead of 'cat' from BashCommand
118
121
  """,
119
122
  ),
120
123
  ToolParam(
@@ -247,7 +250,9 @@ async def handle_call_tool(
247
250
  - Always read relevant files before editing.
248
251
  - Do not provide code snippets unless asked by the user, instead directly add/edit the code.
249
252
  - Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
250
-
253
+ - 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 shell tools.
254
+ - Do not use Ctrl-c or Ctrl-z or interrupt commands without asking the user, because often the program don't show any update but they still are running.
255
+ - Do not use echo to write multi-line files, always use FileEdit tool to update a code.
251
256
 
252
257
  Additional instructions:
253
258
  Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
@@ -172,7 +172,7 @@ def loop(
172
172
  - Do not use interactive commands like nano. Prefer writing simpler commands.
173
173
  - Status of the command and the current working directory will always be returned at the end.
174
174
  - Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
175
- - The first line might be `(...truncated)` if the output is too long.
175
+ - The first or the last line might be `(...truncated)` if the output is too long.
176
176
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
177
177
  - The control will return to you in 5 seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
178
178
  - Run long running commands in background using screen instead of "&".
@@ -192,6 +192,7 @@ def loop(
192
192
  description="""
193
193
  - Read full file content
194
194
  - Provide absolute file path only
195
+ - Use this instead of 'cat' from BashCommand
195
196
  """,
196
197
  ),
197
198
  openai.pydantic_function_tool(
wcgw/client/tools.py CHANGED
@@ -13,7 +13,7 @@ import threading
13
13
  import importlib.metadata
14
14
  import time
15
15
  import traceback
16
- from tempfile import TemporaryDirectory
16
+ from tempfile import NamedTemporaryFile, TemporaryDirectory
17
17
  from typing import (
18
18
  Callable,
19
19
  Literal,
@@ -42,7 +42,7 @@ import rich
42
42
  import pyte
43
43
  from dotenv import load_dotenv
44
44
 
45
- import openai
45
+ from syntax_checker import check_syntax
46
46
  from openai import OpenAI
47
47
  from openai.types.chat import (
48
48
  ChatCompletionMessageParam,
@@ -50,7 +50,7 @@ from openai.types.chat import (
50
50
  ChatCompletionMessage,
51
51
  ParsedChatCompletionMessage,
52
52
  )
53
- from nltk.metrics.distance import edit_distance # type: ignore[import-untyped]
53
+ from difflib import SequenceMatcher
54
54
 
55
55
  from ..types_ import (
56
56
  BashCommand,
@@ -183,6 +183,7 @@ class BashState:
183
183
  self._is_in_docker: Optional[str] = ""
184
184
  self._cwd: str = os.getcwd()
185
185
  self._shell = start_shell()
186
+ self._whitelist_for_overwrite: set[str] = set()
186
187
 
187
188
  # Get exit info to ensure shell is ready
188
189
  _get_exit_code(self._shell)
@@ -235,6 +236,13 @@ class BashState:
235
236
  )
236
237
  return "Not pending"
237
238
 
239
+ @property
240
+ def whitelist_for_overwrite(self) -> set[str]:
241
+ return self._whitelist_for_overwrite
242
+
243
+ def add_to_whitelist_for_overwrite(self, file_path: str) -> None:
244
+ self._whitelist_for_overwrite.add(file_path)
245
+
238
246
 
239
247
  BASH_STATE = BashState()
240
248
 
@@ -269,7 +277,7 @@ def update_repl_prompt(command: str) -> bool:
269
277
  BASH_STATE.shell.sendintr()
270
278
  index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
271
279
  if index == 0:
272
- return False
280
+ return True
273
281
  before = BASH_STATE.shell.before or ""
274
282
  assert before, "Something went wrong updating repl prompt"
275
283
  PROMPT = before.split("\n")[-1].strip()
@@ -301,6 +309,34 @@ def get_status() -> str:
301
309
  return status.rstrip()
302
310
 
303
311
 
312
+ T = TypeVar("T")
313
+
314
+
315
+ def save_out_of_context(
316
+ tokens: list[T],
317
+ max_tokens: int,
318
+ suffix: str,
319
+ tokens_converted: Callable[[list[T]], str],
320
+ ) -> tuple[str, list[Path]]:
321
+ file_contents = list[str]()
322
+ for i in range(0, len(tokens), max_tokens):
323
+ file_contents.append(tokens_converted(tokens[i : i + max_tokens]))
324
+
325
+ if len(file_contents) == 1:
326
+ return file_contents[0], []
327
+
328
+ rest_paths = list[Path]()
329
+ for i, content in enumerate(file_contents):
330
+ if i == 0:
331
+ continue
332
+ file_path = NamedTemporaryFile(delete=False, suffix=suffix).name
333
+ with open(file_path, "w") as f:
334
+ f.write(content)
335
+ rest_paths.append(Path(file_path))
336
+
337
+ return file_contents[0], rest_paths
338
+
339
+
304
340
  def execute_bash(
305
341
  enc: tiktoken.Encoding,
306
342
  bash_arg: BashCommand | BashInteraction,
@@ -412,7 +448,7 @@ def execute_bash(
412
448
  tokens = enc.encode(text)
413
449
 
414
450
  if max_tokens and len(tokens) >= max_tokens:
415
- text = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
451
+ text = "(...truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
416
452
 
417
453
  if is_interrupt:
418
454
  text = (
@@ -441,7 +477,7 @@ Otherwise, you may want to try Ctrl-c again or program specific exit interactive
441
477
 
442
478
  tokens = enc.encode(output)
443
479
  if max_tokens and len(tokens) >= max_tokens:
444
- output = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
480
+ output = "(...truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
445
481
 
446
482
  try:
447
483
  exit_status = get_status()
@@ -490,8 +526,6 @@ class ImageData(BaseModel):
490
526
 
491
527
  Param = ParamSpec("Param")
492
528
 
493
- T = TypeVar("T")
494
-
495
529
 
496
530
  def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
497
531
  def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
@@ -537,20 +571,19 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
537
571
  else:
538
572
  path_ = writefile.file_path
539
573
 
540
- error_on_exist = (
541
- not (
542
- len(TOOL_CALLS) > 1
543
- and isinstance(TOOL_CALLS[-2], FileEdit)
544
- and TOOL_CALLS[-2].file_path == path_
545
- )
546
- and error_on_exist
547
- )
548
-
574
+ error_on_exist_ = error_on_exist and path_ not in BASH_STATE.whitelist_for_overwrite
575
+ add_overwrite_warning = ""
549
576
  if not BASH_STATE.is_in_docker:
550
- if error_on_exist and os.path.exists(path_):
551
- file_data = Path(path_).read_text()
552
- if file_data:
553
- return f"Error: can't write to existing file {path_}, use other functions to edit the file"
577
+ if (error_on_exist or error_on_exist_) and os.path.exists(path_):
578
+ content = Path(path_).read_text().strip()
579
+ if content:
580
+ if error_on_exist_:
581
+ return f"Error: can't write to existing file {path_}, use other functions to edit the file"
582
+ elif error_on_exist:
583
+ add_overwrite_warning = content
584
+
585
+ # Since we've already errored once, add this to whitelist
586
+ BASH_STATE.add_to_whitelist_for_overwrite(path_)
554
587
 
555
588
  path = Path(path_)
556
589
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -561,12 +594,19 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
561
594
  except OSError as e:
562
595
  return f"Error: {e}"
563
596
  else:
564
- if error_on_exist:
565
- # Check if it exists using os.system
566
- cmd = f"test -f {shlex.quote(path_)}"
567
- status = os.system(f'docker exec {BASH_STATE.is_in_docker} bash -c "{cmd}"')
568
- if status == 0:
569
- return f"Error: can't write to existing file {path_}, use other functions to edit the file"
597
+ if error_on_exist or error_on_exist_:
598
+ return_code, content, stderr = command_run(
599
+ f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(path_)}",
600
+ timeout=TIMEOUT,
601
+ )
602
+ if return_code != 0 and content.strip():
603
+ if error_on_exist_:
604
+ return f"Error: can't write to existing file {path_}, use other functions to edit the file"
605
+ else:
606
+ add_overwrite_warning = content
607
+
608
+ # Since we've already errored once, add this to whitelist
609
+ BASH_STATE.add_to_whitelist_for_overwrite(path_)
570
610
 
571
611
  with TemporaryDirectory() as tmpdir:
572
612
  tmppath = os.path.join(tmpdir, os.path.basename(path_))
@@ -586,41 +626,75 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
586
626
  if rcode != 0:
587
627
  return f"Error: Write failed with code {rcode}"
588
628
 
629
+ extension = Path(path_).suffix.lstrip(".")
630
+
589
631
  console.print(f"File written to {path_}")
590
- return "Success"
632
+
633
+ warnings = []
634
+ try:
635
+ check = check_syntax(extension, writefile.file_content)
636
+ syntax_errors = check.description
637
+ if syntax_errors:
638
+ console.print(f"W: Syntax errors encountered: {syntax_errors}")
639
+ warnings.append(f"""
640
+ ---
641
+ Warning: tree-sitter reported syntax errors, please re-read the file and fix if any errors.
642
+ Errors:
643
+ {syntax_errors}
644
+ ---
645
+ """)
646
+
647
+ except Exception:
648
+ pass
649
+
650
+ if add_overwrite_warning:
651
+ warnings.append(
652
+ "\n---\nWarning: a file already existed and it's now overwritten. Was it a mistake? If yes please revert your action."
653
+ "Here's the previous content:\n```\n" + add_overwrite_warning + "\n```"
654
+ "\n---\n"
655
+ )
656
+
657
+ return "Success" + "".join(warnings)
591
658
 
592
659
 
593
660
  def find_least_edit_distance_substring(
594
- content: str, find_str: str
595
- ) -> tuple[str, float]:
596
- orig_content_lines = content.split("\n")
597
- content_lines = [
598
- line.strip() for line in orig_content_lines
599
- ] # Remove trailing and leading space for calculating edit distance
661
+ orig_content_lines: list[str], find_lines: list[str]
662
+ ) -> tuple[list[str], str]:
663
+ # Prepare content lines, stripping whitespace and keeping track of original indices
664
+ content_lines = [line.strip() for line in orig_content_lines]
600
665
  new_to_original_indices = {}
601
666
  new_content_lines = []
602
- for i in range(len(content_lines)):
603
- if not content_lines[i]:
667
+ for i, line in enumerate(content_lines):
668
+ if not line:
604
669
  continue
605
- new_content_lines.append(content_lines[i])
670
+ new_content_lines.append(line)
606
671
  new_to_original_indices[len(new_content_lines) - 1] = i
607
672
  content_lines = new_content_lines
608
- find_lines = find_str.split("\n")
609
- find_lines = [
610
- line.strip() for line in find_lines if line.strip()
611
- ] # Remove trailing and leading space for calculating edit distance
612
- # Slide window and find one with sum of edit distance least
613
- min_edit_distance = float("inf")
673
+
674
+ # Prepare find lines, removing empty lines
675
+ find_lines = [line.strip() for line in find_lines if line.strip()]
676
+
677
+ # Initialize variables for best match tracking
678
+ max_similarity = 0.0
614
679
  min_edit_distance_lines = []
680
+ context_lines = []
681
+
682
+ # For each possible starting position in content
615
683
  for i in range(max(1, len(content_lines) - len(find_lines) + 1)):
616
- edit_distance_sum = 0
684
+ # Calculate similarity for the block starting at position i
685
+ block_similarity = 0.0
617
686
  for j in range(len(find_lines)):
618
687
  if (i + j) < len(content_lines):
619
- edit_distance_sum += edit_distance(content_lines[i + j], find_lines[j])
620
- else:
621
- edit_distance_sum += len(find_lines[j])
622
- if edit_distance_sum < min_edit_distance:
623
- min_edit_distance = edit_distance_sum
688
+ # Use SequenceMatcher for more efficient similarity calculation
689
+ similarity = SequenceMatcher(
690
+ None, content_lines[i + j], find_lines[j]
691
+ ).ratio()
692
+ block_similarity += similarity
693
+
694
+ # If this block is more similar than previous best
695
+ if block_similarity > max_similarity:
696
+ max_similarity = block_similarity
697
+ # Map back to original line indices
624
698
  orig_start_index = new_to_original_indices[i]
625
699
  orig_end_index = (
626
700
  new_to_original_indices.get(
@@ -628,34 +702,79 @@ def find_least_edit_distance_substring(
628
702
  )
629
703
  + 1
630
704
  )
705
+ # Get the original lines
631
706
  min_edit_distance_lines = orig_content_lines[
707
+ orig_start_index:orig_end_index
708
+ ]
709
+ # Get context (10 lines before and after)
710
+ context_lines = orig_content_lines[
632
711
  max(0, orig_start_index - 10) : (orig_end_index + 10)
633
712
  ]
634
- return "\n".join(min_edit_distance_lines), min_edit_distance
635
713
 
714
+ return (
715
+ min_edit_distance_lines,
716
+ "\n".join(context_lines),
717
+ )
636
718
 
637
- def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str:
638
- count = content.count(find_lines)
639
- if count == 0:
640
- closest_match, min_edit_distance = find_least_edit_distance_substring(
641
- content, find_lines
642
- )
643
- if min_edit_distance == 0:
644
- return edit_content(content, closest_match, replace_with_lines)
645
- else:
646
- print(
647
- f"Exact match not found, found with whitespace removed edit distance: {min_edit_distance}"
719
+
720
+ def lines_replacer(
721
+ orig_content_lines: list[str], search_lines: list[str], replace_lines: list[str]
722
+ ) -> str:
723
+ # Validation for empty search
724
+ search_lines = list(filter(None, [x.strip() for x in search_lines]))
725
+
726
+ # Create mapping of non-empty lines to original indices
727
+ new_to_original_indices = []
728
+ new_content_lines = []
729
+ for i, line in enumerate(orig_content_lines):
730
+ stripped = line.strip()
731
+ if not stripped:
732
+ continue
733
+ new_content_lines.append(stripped)
734
+ new_to_original_indices.append(i)
735
+
736
+ if not new_content_lines and not search_lines:
737
+ return "\n".join(replace_lines)
738
+ elif not search_lines:
739
+ raise ValueError("Search block is empty")
740
+ elif not new_content_lines:
741
+ raise ValueError("File content is empty")
742
+
743
+ # Search for matching block
744
+ for i in range(len(new_content_lines) - len(search_lines) + 1):
745
+ if all(
746
+ new_content_lines[i + j] == search_lines[j]
747
+ for j in range(len(search_lines))
748
+ ):
749
+ start_idx = new_to_original_indices[i]
750
+ end_idx = new_to_original_indices[i + len(search_lines) - 1] + 1
751
+ return "\n".join(
752
+ orig_content_lines[:start_idx]
753
+ + replace_lines
754
+ + orig_content_lines[end_idx:]
648
755
  )
649
- raise Exception(
650
- f"""Error: no match found for the provided search block.
651
- Requested search block: \n```\n{find_lines}\n```
652
- Possible relevant section in the file:\n---\n```\n{closest_match}\n```\n---\nFile not edited
653
- \nPlease retry with exact search. Re-read the file if unsure.
654
- """
655
- )
656
756
 
657
- content = content.replace(find_lines, replace_with_lines, 1)
658
- return content
757
+ raise ValueError("Search block not found in content")
758
+
759
+
760
+ def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str:
761
+ replace_with_lines_ = replace_with_lines.split("\n")
762
+ find_lines_ = find_lines.split("\n")
763
+ content_lines_ = content.split("\n")
764
+ try:
765
+ return lines_replacer(content_lines_, find_lines_, replace_with_lines_)
766
+ except ValueError:
767
+ pass
768
+
769
+ _, context_lines = find_least_edit_distance_substring(content_lines_, find_lines_)
770
+
771
+ raise Exception(
772
+ f"""Error: no match found for the provided search block.
773
+ Requested search block: \n```\n{find_lines}\n```
774
+ Possible relevant section in the file:\n---\n```\n{context_lines}\n```\n---\nFile not edited
775
+ \nPlease retry with exact search. Re-read the file if unsure.
776
+ """
777
+ )
659
778
 
660
779
 
661
780
  def do_diff_edit(fedit: FileEdit) -> str:
@@ -686,6 +805,9 @@ def _do_diff_edit(fedit: FileEdit) -> str:
686
805
  else:
687
806
  path_ = fedit.file_path
688
807
 
808
+ # The LLM is now aware that the file exists
809
+ BASH_STATE.add_to_whitelist_for_overwrite(path_)
810
+
689
811
  if not BASH_STATE.is_in_docker:
690
812
  if not os.path.exists(path_):
691
813
  raise Exception(f"Error: file {path_} does not exist")
@@ -766,6 +888,22 @@ def _do_diff_edit(fedit: FileEdit) -> str:
766
888
  if rcode != 0:
767
889
  raise Exception(f"Error: Write failed with code {rcode}")
768
890
 
891
+ syntax_errors = ""
892
+ extension = Path(path_).suffix.lstrip(".")
893
+ try:
894
+ check = check_syntax(extension, apply_diff_to)
895
+ syntax_errors = check.description
896
+ if syntax_errors:
897
+ console.print(f"W: Syntax errors encountered: {syntax_errors}")
898
+ return f"""Wrote file succesfully.
899
+ ---
900
+ However, tree-sitter reported syntax errors, please re-read the file and fix if there are any errors.
901
+ Errors:
902
+ {syntax_errors}
903
+ """
904
+ except Exception:
905
+ pass
906
+
769
907
  return "Success"
770
908
 
771
909
 
@@ -876,7 +1014,7 @@ def get_tool_output(
876
1014
  output = ask_confirmation(arg), 0.0
877
1015
  elif isinstance(arg, (BashCommand | BashInteraction)):
878
1016
  console.print("Calling execute bash tool")
879
- output = execute_bash(enc, arg, max_tokens, None)
1017
+ output = execute_bash(enc, arg, max_tokens, arg.wait_for_seconds)
880
1018
  elif isinstance(arg, WriteIfEmpty):
881
1019
  console.print("Calling write file tool")
882
1020
  output = write_file(arg, True), 0
@@ -1045,6 +1183,8 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
1045
1183
  if not os.path.isabs(readfile.file_path):
1046
1184
  return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
1047
1185
 
1186
+ BASH_STATE.add_to_whitelist_for_overwrite(readfile.file_path)
1187
+
1048
1188
  if not BASH_STATE.is_in_docker:
1049
1189
  path = Path(readfile.file_path)
1050
1190
  if not path.exists():
@@ -1066,7 +1206,14 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
1066
1206
  if max_tokens is not None:
1067
1207
  tokens = default_enc.encode(content)
1068
1208
  if len(tokens) > max_tokens:
1069
- content = default_enc.decode(tokens[: max_tokens - 5])
1070
- content += "\n...(truncated)"
1209
+ content, rest = save_out_of_context(
1210
+ tokens,
1211
+ max_tokens - 100,
1212
+ Path(readfile.file_path).suffix,
1213
+ default_enc.decode,
1214
+ )
1215
+ if rest:
1216
+ rest_ = "\n".join(map(str, rest))
1217
+ content += f"\n(...truncated)\n---\nI've split the rest of the file into multiple files. Here are the remaining splits, please read them:\n{rest_}"
1071
1218
 
1072
1219
  return content
wcgw/types_.py CHANGED
@@ -5,6 +5,7 @@ from pydantic import BaseModel
5
5
 
6
6
  class BashCommand(BaseModel):
7
7
  command: str
8
+ wait_for_seconds: Optional[int] = None
8
9
 
9
10
 
10
11
  Specials = Literal[
@@ -17,6 +18,7 @@ class BashInteraction(BaseModel):
17
18
  send_text: Optional[str] = None
18
19
  send_specials: Optional[Sequence[Specials]] = None
19
20
  send_ascii: Optional[Sequence[int]] = None
21
+ wait_for_seconds: Optional[int] = None
20
22
 
21
23
 
22
24
  class ReadImage(BaseModel):
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: Shell and coding agent on claude and chatgpt
5
5
  Project-URL: Homepage, https://github.com/rusiaaman/wcgw
6
6
  Author-email: Aman Rusia <gapypi@arcfu.com>
@@ -9,7 +9,6 @@ Requires-Dist: anthropic>=0.39.0
9
9
  Requires-Dist: fastapi>=0.115.0
10
10
  Requires-Dist: humanize>=4.11.0
11
11
  Requires-Dist: mcp
12
- Requires-Dist: nltk>=3.9.1
13
12
  Requires-Dist: openai>=1.46.0
14
13
  Requires-Dist: petname>=2.6
15
14
  Requires-Dist: pexpect>=4.9.0
@@ -19,6 +18,7 @@ Requires-Dist: python-dotenv>=1.0.1
19
18
  Requires-Dist: rich>=13.8.1
20
19
  Requires-Dist: semantic-version>=2.10.0
21
20
  Requires-Dist: shell>=1.0.1
21
+ Requires-Dist: syntax-checker==0.2.10
22
22
  Requires-Dist: tiktoken==0.7.0
23
23
  Requires-Dist: toml>=0.10.2
24
24
  Requires-Dist: typer>=0.12.5
@@ -1,22 +1,22 @@
1
1
  wcgw/__init__.py,sha256=9K2QW7QuSLhMTVbKbBYd9UUp-ZyrfBrxcjuD_xk458k,118
2
- wcgw/types_.py,sha256=5QM97K3kOJ7vNBgZ1NYmKPqwSe9WfzDImAIvjbRcClo,1772
2
+ wcgw/types_.py,sha256=9h0-UKS4emx12eI24VSfgvz8WW0p5hwxFwzq8Wvbk6w,1858
3
3
  wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  wcgw/client/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
5
- wcgw/client/anthropic_client.py,sha256=68JNQAdH9Cg5lWTMKX3SvGNbYqPGyGe02VWWUbLaNss,20938
5
+ wcgw/client/anthropic_client.py,sha256=ymMi2Kmkcso_6PT8iuwsqLz4Ne6oTvId3OWBB4La3gc,21147
6
6
  wcgw/client/cli.py,sha256=-z0kpDAW3mzfQrQeZfaVJhBCAQY3HXnt9GdgQ8s-u0Y,1003
7
7
  wcgw/client/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
8
8
  wcgw/client/computer_use.py,sha256=35NKAlMrxwD0TBlMMRnbCwz4g8TBRGOlcy-cmS-yJ_A,15247
9
9
  wcgw/client/diff-instructions.txt,sha256=s5AJKG23JsjwRYhFZFQVvwDpF67vElawrmdXwvukR1A,1683
10
- wcgw/client/openai_client.py,sha256=xfB96A4qZM7-CUJhyDMqX9rUIzhCw4AtV9ryTKE8SOM,17885
10
+ wcgw/client/openai_client.py,sha256=uJ2l9NXsZuipUcJYR_bFcNNmNlfnCvPm6-M-LiVSVts,17942
11
11
  wcgw/client/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
12
12
  wcgw/client/sys_utils.py,sha256=GajPntKhaTUMn6EOmopENWZNR2G_BJyuVbuot0x6veI,1376
13
- wcgw/client/tools.py,sha256=e5ul5ZNjvP7UJ8-gubHiNCYjcBIjAPkbkBckKnwKJp4,35724
13
+ wcgw/client/tools.py,sha256=YVzEQeJdrUpmGmEgNNRsIyjbP2TFWmpO8EjXaB28Ih8,40692
14
14
  wcgw/client/mcp_server/Readme.md,sha256=I8N4dHkTUVGNQ63BQkBMBhCCBTgqGOSF_pUR6iOEiUk,2495
15
15
  wcgw/client/mcp_server/__init__.py,sha256=hyPPwO9cabAJsOMWhKyat9yl7OlSmIobaoAZKHu3DMc,381
16
- wcgw/client/mcp_server/server.py,sha256=DCclYqC8mRzCti2pztnm8ZkwGeXnzzNlkvi5Yh02D20,11226
16
+ wcgw/client/mcp_server/server.py,sha256=GfGOl1baqV7gmSLQtiJ_px-KyERNuG7-XCaLgkHdcEA,11858
17
17
  wcgw/relay/serve.py,sha256=KLYjTvM9CfqdxgFOfHM8LUkFGZ9kKyyJunpNdEIFQUk,8766
18
18
  wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
19
- wcgw-2.2.1.dist-info/METADATA,sha256=ZI81P-DAaTxkmZmU8tBJ8tYAchOEJ3p0VeRGXpWi3IQ,7939
20
- wcgw-2.2.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
21
- wcgw-2.2.1.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
22
- wcgw-2.2.1.dist-info/RECORD,,
19
+ wcgw-2.3.0.dist-info/METADATA,sha256=w8yIw6ra2ianLHW54ToRzWbZ_oNx4fxaXjbDD2e0wQA,7950
20
+ wcgw-2.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ wcgw-2.3.0.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
22
+ wcgw-2.3.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any