wcgw 2.4.3__py3-none-any.whl → 2.5.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.

@@ -29,7 +29,7 @@ from ..types_ import (
29
29
  FileEdit,
30
30
  Keyboard,
31
31
  Mouse,
32
- ReadFile,
32
+ ReadFiles,
33
33
  ReadImage,
34
34
  ResetShell,
35
35
  ScreenShot,
@@ -41,12 +41,7 @@ from .common import CostData
41
41
  from .tools import ImageData
42
42
  from .computer_use import Computer
43
43
 
44
- from .tools import (
45
- DoneFlag,
46
- get_tool_output,
47
- which_tool_name,
48
- )
49
- import tiktoken
44
+ from .tools import DoneFlag, get_tool_output, which_tool_name, default_enc
50
45
 
51
46
  from urllib import parse
52
47
  import subprocess
@@ -156,10 +151,6 @@ def loop(
156
151
 
157
152
  limit = 1
158
153
 
159
- enc = tiktoken.encoding_for_model(
160
- "gpt-4o-2024-08-06",
161
- )
162
-
163
154
  tools = [
164
155
  ToolParam(
165
156
  input_schema=BashCommand.model_json_schema(),
@@ -192,12 +183,11 @@ def loop(
192
183
  """,
193
184
  ),
194
185
  ToolParam(
195
- input_schema=ReadFile.model_json_schema(),
196
- name="ReadFile",
186
+ input_schema=ReadFiles.model_json_schema(),
187
+ name="ReadFiles",
197
188
  description="""
198
- - Read full file content
199
- - Provide absolute file path only
200
- - Use this instead of 'cat' from BashCommand
189
+ - Read full file content of one or more files.
190
+ - Provide absolute file paths only
201
191
  """,
202
192
  ),
203
193
  ToolParam(
@@ -451,7 +441,7 @@ System information:
451
441
  try:
452
442
  output_or_dones, _ = get_tool_output(
453
443
  tool_parsed,
454
- enc,
444
+ default_enc,
455
445
  limit - cost,
456
446
  loop,
457
447
  max_tokens=8000,
wcgw/client/common.py CHANGED
@@ -38,7 +38,9 @@ def discard_input() -> None:
38
38
  while True:
39
39
  # Check if there is input to be read
40
40
  if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
41
- sys.stdin.read(1) # Read one character at a time to flush the input buffer
41
+ sys.stdin.read(
42
+ 1
43
+ ) # Read one character at a time to flush the input buffer
42
44
  else:
43
45
  break
44
46
  finally:
@@ -21,7 +21,7 @@ from ...types_ import (
21
21
  FileEdit,
22
22
  Keyboard,
23
23
  Mouse,
24
- ReadFile,
24
+ ReadFiles,
25
25
  ReadImage,
26
26
  ResetShell,
27
27
  Initialize,
@@ -76,7 +76,7 @@ async def handle_list_tools() -> list[types.Tool]:
76
76
  inputSchema=Initialize.model_json_schema(),
77
77
  name="Initialize",
78
78
  description="""
79
- - Always call this at the start of the conversation before anything else.
79
+ - Always call this at the start of the conversation before using any of the shell tools from wcgw.
80
80
  """,
81
81
  ),
82
82
  ToolParam(
@@ -92,6 +92,7 @@ async def handle_list_tools() -> list[types.Tool]:
92
92
  - The control will return to you in {SLEEP_TIME_MAX_S} seconds regardless of the status. For heavy commands, keep checking status using BashInteraction till they are finished.
93
93
  - Run long running commands in background using screen instead of "&".
94
94
  - Use longer wait_for_seconds if the command is expected to run for a long time.
95
+ - Do not use 'cat' to read files, use ReadFiles tool instead.
95
96
  """,
96
97
  ),
97
98
  ToolParam(
@@ -110,12 +111,11 @@ async def handle_list_tools() -> list[types.Tool]:
110
111
  """,
111
112
  ),
112
113
  ToolParam(
113
- inputSchema=ReadFile.model_json_schema(),
114
- name="ReadFile",
114
+ inputSchema=ReadFiles.model_json_schema(),
115
+ name="ReadFiles",
115
116
  description="""
116
- - Read full file content
117
- - Provide absolute file path only
118
- - Use this instead of 'cat' from BashCommand
117
+ - Read full file content of one or more files.
118
+ - Provide absolute file paths only
119
119
  """,
120
120
  ),
121
121
  ToolParam(
@@ -17,6 +17,7 @@ from openai.types.chat import (
17
17
  )
18
18
  import rich
19
19
  import petname # type: ignore[import-untyped]
20
+ import tokenizers # type: ignore[import-untyped]
20
21
  from typer import Typer
21
22
  import uuid
22
23
 
@@ -26,7 +27,7 @@ from ..types_ import (
26
27
  WriteIfEmpty,
27
28
  FileEdit,
28
29
  ReadImage,
29
- ReadFile,
30
+ ReadFiles,
30
31
  ResetShell,
31
32
  )
32
33
 
@@ -40,7 +41,6 @@ from .tools import (
40
41
  get_tool_output,
41
42
  which_tool,
42
43
  )
43
- import tiktoken
44
44
 
45
45
  from urllib import parse
46
46
  import subprocess
@@ -160,9 +160,7 @@ def loop(
160
160
  config.cost_limit = limit
161
161
  limit = config.cost_limit
162
162
 
163
- enc = tiktoken.encoding_for_model(
164
- config.model if not config.model.startswith("o1") else "gpt-4o"
165
- )
163
+ enc = tokenizers.Tokenizer.from_pretrained("Xenova/gpt-4o")
166
164
 
167
165
  tools = [
168
166
  openai.pydantic_function_tool(
@@ -188,11 +186,10 @@ def loop(
188
186
  - Only one of send_text, send_specials, send_ascii should be provided.""",
189
187
  ),
190
188
  openai.pydantic_function_tool(
191
- ReadFile,
189
+ ReadFiles,
192
190
  description="""
193
- - Read full file content
194
- - Provide absolute file path only
195
- - Use this instead of 'cat' from BashCommand
191
+ - Read full file content of one or more files.
192
+ - Provide absolute file paths only
196
193
  """,
197
194
  ),
198
195
  openai.pydantic_function_tool(
@@ -15,7 +15,7 @@ from openai.types.chat import (
15
15
  ParsedChatCompletionMessage,
16
16
  )
17
17
  import rich
18
- import tiktoken
18
+ from tokenizers import Tokenizer # type: ignore[import-untyped]
19
19
  from typer import Typer
20
20
  import uuid
21
21
 
@@ -23,7 +23,7 @@ from .common import CostData, History
23
23
 
24
24
 
25
25
  def get_input_cost(
26
- cost_map: CostData, enc: tiktoken.Encoding, history: History
26
+ cost_map: CostData, enc: Tokenizer, history: History
27
27
  ) -> tuple[float, int]:
28
28
  input_tokens = 0
29
29
  for msg in history:
@@ -31,8 +31,8 @@ def get_input_cost(
31
31
  refusal = msg.get("refusal")
32
32
  if isinstance(content, list):
33
33
  for part in content:
34
- if 'text' in part:
35
- input_tokens += len(enc.encode(part['text']))
34
+ if "text" in part:
35
+ input_tokens += len(enc.encode(part["text"]))
36
36
  elif content is None:
37
37
  if refusal is None:
38
38
  raise ValueError("Expected content or refusal to be present")
@@ -47,7 +47,7 @@ def get_input_cost(
47
47
 
48
48
  def get_output_cost(
49
49
  cost_map: CostData,
50
- enc: tiktoken.Encoding,
50
+ enc: Tokenizer,
51
51
  item: ChatCompletionMessage | ChatCompletionMessageParam,
52
52
  ) -> tuple[float, int]:
53
53
  if isinstance(item, ChatCompletionMessage):
wcgw/client/tools.py CHANGED
@@ -19,14 +19,14 @@ from typing import (
19
19
  TypeVar,
20
20
  )
21
21
  import uuid
22
- import humanize
22
+
23
23
  from pydantic import BaseModel, TypeAdapter
24
24
  import typer
25
25
  from .computer_use import run_computer_tool
26
26
  from websockets.sync.client import connect as syncconnect
27
27
 
28
28
  import os
29
- import tiktoken
29
+ import tokenizers # type: ignore
30
30
  import pexpect
31
31
  from typer import Typer
32
32
  import websockets
@@ -47,7 +47,7 @@ from ..types_ import (
47
47
  FileEditFindReplace,
48
48
  FileEdit,
49
49
  Initialize,
50
- ReadFile,
50
+ ReadFiles,
51
51
  ReadImage,
52
52
  ResetShell,
53
53
  Mouse,
@@ -259,9 +259,17 @@ class BashState:
259
259
  def get_pending_for(self) -> str:
260
260
  if isinstance(self._state, datetime.datetime):
261
261
  timedelta = datetime.datetime.now() - self._state
262
- return humanize.naturaldelta(
263
- timedelta + datetime.timedelta(seconds=TIMEOUT)
262
+ return (
263
+ str(
264
+ int(
265
+ (
266
+ timedelta + datetime.timedelta(seconds=TIMEOUT)
267
+ ).total_seconds()
268
+ )
269
+ )
270
+ + " seconds"
264
271
  )
272
+
265
273
  return "Not pending"
266
274
 
267
275
  @property
@@ -279,16 +287,24 @@ class BashState:
279
287
  BASH_STATE = BashState()
280
288
 
281
289
 
282
- def initial_info() -> str:
290
+ def initialize(workspace_dir: str = "") -> str:
291
+ reset_shell()
292
+ if workspace_dir:
293
+ BASH_STATE.shell.sendline(f"cd {shlex.quote(workspace_dir)}")
294
+ BASH_STATE.shell.expect(PROMPT, timeout=0.2)
295
+ BASH_STATE.update_cwd()
296
+
283
297
  uname_sysname = os.uname().sysname
284
298
  uname_machine = os.uname().machine
285
- return f"""
299
+
300
+ output = f"""
286
301
  System: {uname_sysname}
287
302
  Machine: {uname_machine}
288
303
  Current working directory: {BASH_STATE.cwd}
289
- wcgw version: {importlib.metadata.version("wcgw")}
290
304
  """
291
305
 
306
+ return output
307
+
292
308
 
293
309
  def reset_shell() -> str:
294
310
  BASH_STATE.reset()
@@ -345,29 +361,11 @@ def get_status() -> str:
345
361
  T = TypeVar("T")
346
362
 
347
363
 
348
- def save_out_of_context(
349
- tokens: list[T],
350
- max_tokens: int,
351
- suffix: str,
352
- tokens_converted: Callable[[list[T]], str],
353
- ) -> tuple[str, list[Path]]:
354
- file_contents = list[str]()
355
- for i in range(0, len(tokens), max_tokens):
356
- file_contents.append(tokens_converted(tokens[i : i + max_tokens]))
357
-
358
- if len(file_contents) == 1:
359
- return file_contents[0], []
360
-
361
- rest_paths = list[Path]()
362
- for i, content in enumerate(file_contents):
363
- if i == 0:
364
- continue
365
- file_path = NamedTemporaryFile(delete=False, suffix=suffix).name
366
- with open(file_path, "w") as f:
367
- f.write(content)
368
- rest_paths.append(Path(file_path))
369
-
370
- return file_contents[0], rest_paths
364
+ def save_out_of_context(content: str, suffix: str) -> str:
365
+ file_path = NamedTemporaryFile(delete=False, suffix=suffix).name
366
+ with open(file_path, "w") as f:
367
+ f.write(content)
368
+ return file_path
371
369
 
372
370
 
373
371
  def rstrip(lines: list[str]) -> str:
@@ -404,7 +402,7 @@ def is_status_check(arg: BashInteraction | BashCommand) -> bool:
404
402
 
405
403
 
406
404
  def execute_bash(
407
- enc: tiktoken.Encoding,
405
+ enc: tokenizers.Tokenizer,
408
406
  bash_arg: BashCommand | BashInteraction,
409
407
  max_tokens: Optional[int],
410
408
  timeout_s: Optional[float],
@@ -549,7 +547,7 @@ def execute_bash(
549
547
 
550
548
  if max_tokens and len(tokens) >= max_tokens:
551
549
  incremental_text = "(...truncated)\n" + enc.decode(
552
- tokens[-(max_tokens - 1) :]
550
+ tokens.ids[-(max_tokens - 1) :]
553
551
  )
554
552
 
555
553
  if is_interrupt:
@@ -573,12 +571,9 @@ def execute_bash(
573
571
  output = _incremental_text(BASH_STATE.shell.before, BASH_STATE.pending_output)
574
572
  BASH_STATE.set_repl()
575
573
 
576
- if is_interrupt:
577
- return "Interrupt successful", 0.0
578
-
579
574
  tokens = enc.encode(output)
580
575
  if max_tokens and len(tokens) >= max_tokens:
581
- output = "(...truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
576
+ output = "(...truncated)\n" + enc.decode(tokens.ids[-(max_tokens - 1) :])
582
577
 
583
578
  try:
584
579
  exit_status = get_status()
@@ -638,6 +633,19 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
638
633
  return wrapper
639
634
 
640
635
 
636
+ def truncate_if_over(content: str, max_tokens: Optional[int]) -> str:
637
+ if max_tokens and max_tokens > 0:
638
+ tokens = default_enc.encode(content)
639
+ n_tokens = len(tokens)
640
+ if n_tokens > max_tokens:
641
+ content = (
642
+ default_enc.decode(tokens.ids[: max(0, max_tokens - 100)])
643
+ + "\n(...truncated)"
644
+ )
645
+
646
+ return content
647
+
648
+
641
649
  def read_image_from_shell(file_path: str) -> ImageData:
642
650
  if not os.path.isabs(file_path):
643
651
  file_path = os.path.join(BASH_STATE.cwd, file_path)
@@ -666,7 +674,25 @@ def read_image_from_shell(file_path: str) -> ImageData:
666
674
  return ImageData(media_type=image_type, data=image_b64) # type: ignore
667
675
 
668
676
 
669
- def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
677
+ def get_context_for_errors(
678
+ errors: list[tuple[int, int]], file_content: str, max_tokens: Optional[int]
679
+ ) -> str:
680
+ file_lines = file_content.split("\n")
681
+ min_line_num = max(0, min([error[0] for error in errors]) - 10)
682
+ max_line_num = min(len(file_lines), max([error[0] for error in errors]) + 10)
683
+ context_lines = file_lines[min_line_num:max_line_num]
684
+ context = "\n".join(context_lines)
685
+
686
+ if max_tokens is not None and max_tokens > 0:
687
+ ntokens = len(default_enc.encode(context))
688
+ if ntokens > max_tokens:
689
+ return "Please re-read the file to understand the context"
690
+ return f"Here's relevant snippet from the file where the syntax errors occured:\n```\n{context}\n```"
691
+
692
+
693
+ def write_file(
694
+ writefile: WriteIfEmpty, error_on_exist: bool, max_tokens: Optional[int]
695
+ ) -> str:
670
696
  if not os.path.isabs(writefile.file_path):
671
697
  return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
672
698
  else:
@@ -678,9 +704,14 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
678
704
  if (error_on_exist or error_on_exist_) and os.path.exists(path_):
679
705
  content = Path(path_).read_text().strip()
680
706
  if content:
707
+ content = truncate_if_over(content, max_tokens)
708
+
681
709
  if error_on_exist_:
682
- return f"Error: can't write to existing file {path_}, use other functions to edit the file"
683
- elif error_on_exist:
710
+ return (
711
+ f"Error: can't write to existing file {path_}, use other functions to edit the file"
712
+ + f"\nHere's the existing content:\n```\n{content}\n```"
713
+ )
714
+ else:
684
715
  add_overwrite_warning = content
685
716
 
686
717
  # Since we've already errored once, add this to whitelist
@@ -701,8 +732,13 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
701
732
  timeout=TIMEOUT,
702
733
  )
703
734
  if return_code != 0 and content.strip():
735
+ content = truncate_if_over(content, max_tokens)
736
+
704
737
  if error_on_exist_:
705
- return f"Error: can't write to existing file {path_}, use other functions to edit the file"
738
+ return (
739
+ f"Error: can't write to existing file {path_}, use other functions to edit the file"
740
+ + f"\nHere's the existing content:\n```\n{content}\n```"
741
+ )
706
742
  else:
707
743
  add_overwrite_warning = content
708
744
 
@@ -735,13 +771,19 @@ def write_file(writefile: WriteIfEmpty, error_on_exist: bool) -> str:
735
771
  try:
736
772
  check = check_syntax(extension, writefile.file_content)
737
773
  syntax_errors = check.description
774
+
738
775
  if syntax_errors:
776
+ context_for_errors = get_context_for_errors(
777
+ check.errors, writefile.file_content, max_tokens
778
+ )
739
779
  console.print(f"W: Syntax errors encountered: {syntax_errors}")
740
780
  warnings.append(f"""
741
781
  ---
742
- Warning: tree-sitter reported syntax errors, please re-read the file and fix if any errors.
743
- Errors:
782
+ Warning: tree-sitter reported syntax errors
783
+ Syntax errors:
744
784
  {syntax_errors}
785
+
786
+ {context_for_errors}
745
787
  ---
746
788
  """)
747
789
 
@@ -751,8 +793,10 @@ Errors:
751
793
  if add_overwrite_warning:
752
794
  warnings.append(
753
795
  "\n---\nWarning: a file already existed and it's now overwritten. Was it a mistake? If yes please revert your action."
754
- "Here's the previous content:\n```\n" + add_overwrite_warning + "\n```"
755
796
  "\n---\n"
797
+ + "Here's the previous content:\n```\n"
798
+ + add_overwrite_warning
799
+ + "\n```"
756
800
  )
757
801
 
758
802
  return "Success" + "".join(warnings)
@@ -878,9 +922,9 @@ def edit_content(content: str, find_lines: str, replace_with_lines: str) -> str:
878
922
  )
879
923
 
880
924
 
881
- def do_diff_edit(fedit: FileEdit) -> str:
925
+ def do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
882
926
  try:
883
- return _do_diff_edit(fedit)
927
+ return _do_diff_edit(fedit, max_tokens)
884
928
  except Exception as e:
885
929
  # Try replacing \"
886
930
  try:
@@ -890,13 +934,13 @@ def do_diff_edit(fedit: FileEdit) -> str:
890
934
  '\\"', '"'
891
935
  ),
892
936
  )
893
- return _do_diff_edit(fedit)
937
+ return _do_diff_edit(fedit, max_tokens)
894
938
  except Exception:
895
939
  pass
896
940
  raise e
897
941
 
898
942
 
899
- def _do_diff_edit(fedit: FileEdit) -> str:
943
+ def _do_diff_edit(fedit: FileEdit, max_tokens: Optional[int]) -> str:
900
944
  console.log(f"Editing file: {fedit.file_path}")
901
945
 
902
946
  if not os.path.isabs(fedit.file_path):
@@ -995,13 +1039,19 @@ def _do_diff_edit(fedit: FileEdit) -> str:
995
1039
  check = check_syntax(extension, apply_diff_to)
996
1040
  syntax_errors = check.description
997
1041
  if syntax_errors:
1042
+ context_for_errors = get_context_for_errors(
1043
+ check.errors, apply_diff_to, max_tokens
1044
+ )
1045
+
998
1046
  console.print(f"W: Syntax errors encountered: {syntax_errors}")
999
1047
  return f"""Wrote file succesfully.
1000
1048
  ---
1001
1049
  However, tree-sitter reported syntax errors, please re-read the file and fix if there are any errors.
1002
- Errors:
1050
+ Syntax errors:
1003
1051
  {syntax_errors}
1004
- """
1052
+
1053
+ {context_for_errors}
1054
+ """
1005
1055
  except Exception:
1006
1056
  pass
1007
1057
 
@@ -1041,7 +1091,7 @@ TOOLS = (
1041
1091
  | AIAssistant
1042
1092
  | DoneFlag
1043
1093
  | ReadImage
1044
- | ReadFile
1094
+ | ReadFiles
1045
1095
  | Initialize
1046
1096
  | Mouse
1047
1097
  | Keyboard
@@ -1076,8 +1126,8 @@ def which_tool_name(name: str) -> Type[TOOLS]:
1076
1126
  return DoneFlag
1077
1127
  elif name == "ReadImage":
1078
1128
  return ReadImage
1079
- elif name == "ReadFile":
1080
- return ReadFile
1129
+ elif name == "ReadFiles":
1130
+ return ReadFiles
1081
1131
  elif name == "Initialize":
1082
1132
  return Initialize
1083
1133
  elif name == "Mouse":
@@ -1097,7 +1147,7 @@ TOOL_CALLS: list[TOOLS] = []
1097
1147
 
1098
1148
  def get_tool_output(
1099
1149
  args: dict[object, object] | TOOLS,
1100
- enc: tiktoken.Encoding,
1150
+ enc: tokenizers.Tokenizer,
1101
1151
  limit: float,
1102
1152
  loop_call: Callable[[str, float], tuple[str, float]],
1103
1153
  max_tokens: Optional[int],
@@ -1118,10 +1168,10 @@ def get_tool_output(
1118
1168
  output = execute_bash(enc, arg, max_tokens, arg.wait_for_seconds)
1119
1169
  elif isinstance(arg, WriteIfEmpty):
1120
1170
  console.print("Calling write file tool")
1121
- output = write_file(arg, True), 0
1171
+ output = write_file(arg, True, max_tokens), 0
1122
1172
  elif isinstance(arg, FileEdit):
1123
1173
  console.print("Calling full file edit tool")
1124
- output = do_diff_edit(arg), 0.0
1174
+ output = do_diff_edit(arg, max_tokens), 0.0
1125
1175
  elif isinstance(arg, DoneFlag):
1126
1176
  console.print("Calling mark finish tool")
1127
1177
  output = mark_finish(arg), 0.0
@@ -1131,17 +1181,15 @@ def get_tool_output(
1131
1181
  elif isinstance(arg, ReadImage):
1132
1182
  console.print("Calling read image tool")
1133
1183
  output = read_image_from_shell(arg.file_path), 0.0
1134
- elif isinstance(arg, ReadFile):
1184
+ elif isinstance(arg, ReadFiles):
1135
1185
  console.print("Calling read file tool")
1136
- output = read_file(arg, max_tokens), 0.0
1186
+ output = read_files(arg.file_paths, max_tokens), 0.0
1137
1187
  elif isinstance(arg, ResetShell):
1138
1188
  console.print("Calling reset shell tool")
1139
1189
  output = reset_shell(), 0.0
1140
1190
  elif isinstance(arg, Initialize):
1141
1191
  console.print("Calling initial info tool")
1142
- # First force reset
1143
- reset_shell()
1144
- output = initial_info(), 0.0
1192
+ output = initialize(), 0.0
1145
1193
  elif isinstance(arg, (Mouse, Keyboard, ScreenShot, GetScreenInfo)):
1146
1194
  console.print(f"Calling {type(arg).__name__} tool")
1147
1195
  outputs_cost = run_computer_tool(arg), 0.0
@@ -1190,7 +1238,9 @@ def get_tool_output(
1190
1238
 
1191
1239
  History = list[ChatCompletionMessageParam]
1192
1240
 
1193
- default_enc = tiktoken.encoding_for_model("gpt-4o")
1241
+ default_enc: tokenizers.Tokenizer = tokenizers.Tokenizer.from_pretrained(
1242
+ "Xenova/claude-tokenizer"
1243
+ )
1194
1244
  curr_cost = 0.0
1195
1245
 
1196
1246
 
@@ -1203,7 +1253,7 @@ class Mdata(BaseModel):
1203
1253
  | FileEditFindReplace
1204
1254
  | FileEdit
1205
1255
  | str
1206
- | ReadFile
1256
+ | ReadFiles
1207
1257
  | Initialize
1208
1258
  )
1209
1259
 
@@ -1276,43 +1326,69 @@ def app(
1276
1326
  register_client(server_url, client_uuid or "")
1277
1327
 
1278
1328
 
1279
- def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
1280
- console.print(f"Reading file: {readfile.file_path}")
1329
+ def read_files(file_paths: list[str], max_tokens: Optional[int]) -> str:
1330
+ message = ""
1331
+ for i, file in enumerate(file_paths):
1332
+ try:
1333
+ content, truncated, tokens = read_file(file, max_tokens)
1334
+ except ValueError as e:
1335
+ message += f"\n{file}: {str(e)}\n"
1336
+ continue
1281
1337
 
1282
- if not os.path.isabs(readfile.file_path):
1283
- return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
1338
+ if max_tokens:
1339
+ max_tokens = max_tokens - tokens
1284
1340
 
1285
- BASH_STATE.add_to_whitelist_for_overwrite(readfile.file_path)
1341
+ message += f"\n``` {file}\n{content}\n"
1342
+
1343
+ if truncated or (max_tokens and max_tokens <= 0):
1344
+ not_reading = file_paths[i + 1 :]
1345
+ if not_reading:
1346
+ message += f'\nNot reading the rest of the files: {", ".join(not_reading)} due to token limit, please call again'
1347
+ break
1348
+ else:
1349
+ message += "```"
1350
+
1351
+ return message
1352
+
1353
+
1354
+ def read_file(file_path: str, max_tokens: Optional[int]) -> tuple[str, bool, int]:
1355
+ console.print(f"Reading file: {file_path}")
1356
+
1357
+ if not os.path.isabs(file_path):
1358
+ raise ValueError(
1359
+ f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
1360
+ )
1361
+
1362
+ BASH_STATE.add_to_whitelist_for_overwrite(file_path)
1286
1363
 
1287
1364
  if not BASH_STATE.is_in_docker:
1288
- path = Path(readfile.file_path)
1365
+ path = Path(file_path)
1289
1366
  if not path.exists():
1290
- return f"Error: file {readfile.file_path} does not exist"
1367
+ raise ValueError(f"Error: file {file_path} does not exist")
1291
1368
 
1292
1369
  with path.open("r") as f:
1293
- content = f.read()
1370
+ content = f.read(10_000_000)
1294
1371
 
1295
1372
  else:
1296
1373
  return_code, content, stderr = command_run(
1297
- f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(readfile.file_path)}",
1374
+ f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(file_path)}",
1298
1375
  timeout=TIMEOUT,
1299
1376
  )
1300
1377
  if return_code != 0:
1301
1378
  raise Exception(
1302
- f"Error: cat {readfile.file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
1379
+ f"Error: cat {file_path} failed with code {return_code}\nstdout: {content}\nstderr: {stderr}"
1303
1380
  )
1304
1381
 
1382
+ truncated = False
1383
+ tokens_counts = 0
1305
1384
  if max_tokens is not None:
1306
1385
  tokens = default_enc.encode(content)
1386
+ tokens_counts = len(tokens)
1307
1387
  if len(tokens) > max_tokens:
1308
- content, rest = save_out_of_context(
1309
- tokens,
1310
- max_tokens - 100,
1311
- Path(readfile.file_path).suffix,
1312
- default_enc.decode,
1388
+ content = default_enc.decode(tokens.ids[:max_tokens])
1389
+ rest = save_out_of_context(
1390
+ default_enc.decode(tokens.ids[max_tokens:]), Path(file_path).suffix
1313
1391
  )
1314
- if rest:
1315
- rest_ = "\n".join(map(str, rest))
1316
- 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_}"
1317
-
1318
- return content
1392
+ content += f"\n(...truncated)\n---\nI've saved the continuation in a new file. Please read: `{rest}`"
1393
+ truncated = True
1394
+ return content, truncated, tokens_counts
wcgw/relay/serve.py CHANGED
@@ -21,7 +21,7 @@ from ..types_ import (
21
21
  FileEditFindReplace,
22
22
  FileEdit,
23
23
  Initialize,
24
- ReadFile,
24
+ ReadFiles,
25
25
  ResetShell,
26
26
  Specials,
27
27
  )
@@ -35,7 +35,7 @@ class Mdata(BaseModel):
35
35
  | ResetShell
36
36
  | FileEditFindReplace
37
37
  | FileEdit
38
- | ReadFile
38
+ | ReadFiles
39
39
  | Initialize
40
40
  | str
41
41
  )
@@ -259,7 +259,7 @@ async def bash_interaction(bash_interaction: BashInteractionWithUUID) -> str:
259
259
  raise fastapi.HTTPException(status_code=500, detail="Timeout error")
260
260
 
261
261
 
262
- class ReadFileWithUUID(ReadFile):
262
+ class ReadFileWithUUID(ReadFiles):
263
263
  user_id: UUID
264
264
 
265
265
 
wcgw/types_.py CHANGED
@@ -31,9 +31,9 @@ class WriteIfEmpty(BaseModel):
31
31
  file_content: str
32
32
 
33
33
 
34
- class ReadFile(BaseModel):
35
- file_path: str # The path to the file to read
36
- type: Literal["ReadFile"]
34
+ class ReadFiles(BaseModel):
35
+ file_paths: list[str]
36
+ type: Literal["ReadFiles"]
37
37
 
38
38
 
39
39
  class FileEditFindReplace(BaseModel):
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 2.4.3
3
+ Version: 2.5.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>
7
+ License-File: LICENSE
7
8
  Requires-Python: <3.13,>=3.11
8
9
  Requires-Dist: anthropic>=0.39.0
9
10
  Requires-Dist: fastapi>=0.115.0
10
- Requires-Dist: humanize>=4.11.0
11
11
  Requires-Dist: openai>=1.46.0
12
12
  Requires-Dist: petname>=2.6
13
13
  Requires-Dist: pexpect>=4.9.0
@@ -18,7 +18,7 @@ Requires-Dist: rich>=13.8.1
18
18
  Requires-Dist: semantic-version>=2.10.0
19
19
  Requires-Dist: shell>=1.0.1
20
20
  Requires-Dist: syntax-checker==0.2.10
21
- Requires-Dist: tiktoken==0.7.0
21
+ Requires-Dist: tokenizers>=0.21.0
22
22
  Requires-Dist: toml>=0.10.2
23
23
  Requires-Dist: typer>=0.12.5
24
24
  Requires-Dist: types-pexpect>=4.9.0.20240806
@@ -1,20 +1,20 @@
1
1
  wcgw/__init__.py,sha256=9K2QW7QuSLhMTVbKbBYd9UUp-ZyrfBrxcjuD_xk458k,118
2
- wcgw/types_.py,sha256=9h0-UKS4emx12eI24VSfgvz8WW0p5hwxFwzq8Wvbk6w,1858
2
+ wcgw/types_.py,sha256=NDWBfzmR89-L7rFuf-9PeCF3jZuQ-WWMLbRVP-qlnJw,1835
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=ymMi2Kmkcso_6PT8iuwsqLz4Ne6oTvId3OWBB4La3gc,21147
5
+ wcgw/client/anthropic_client.py,sha256=kmS93LEVwXCppfbgCsfWe_SVmTAtsI3x8PrP8DJDYMY,21041
6
6
  wcgw/client/cli.py,sha256=-z0kpDAW3mzfQrQeZfaVJhBCAQY3HXnt9GdgQ8s-u0Y,1003
7
- wcgw/client/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
7
+ wcgw/client/common.py,sha256=OCH7Tx64jojz3M3iONUrGMadE07W21DiZs5sOxWX1Qc,1456
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=uJ2l9NXsZuipUcJYR_bFcNNmNlfnCvPm6-M-LiVSVts,17942
11
- wcgw/client/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
10
+ wcgw/client/openai_client.py,sha256=hAoUF4wcD0LqqX9MjqWZux12_HH0EdVognNcRLG6pew,17903
11
+ wcgw/client/openai_utils.py,sha256=KfMB1-p2zDiA7pPWwAVarochf7-qeL1UMgtlDV9DtKA,2662
12
12
  wcgw/client/sys_utils.py,sha256=GajPntKhaTUMn6EOmopENWZNR2G_BJyuVbuot0x6veI,1376
13
- wcgw/client/tools.py,sha256=cOrlfFRkDXBxlB8pAQ8vOds9EYmnRncftoNkGsabRD0,44563
13
+ wcgw/client/tools.py,sha256=HO4xO0D7oxYJGeof0O-GIADc9UkoPwRbmyh9SgAWTMw,47079
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=CNUOAd83lCq0Ed_ZRwd66gIjMFN9VBSO4moTLUPTWwM,11956
17
- wcgw/relay/serve.py,sha256=KLYjTvM9CfqdxgFOfHM8LUkFGZ9kKyyJunpNdEIFQUk,8766
16
+ wcgw/client/mcp_server/server.py,sha256=d8fegYsneGnmzwqCCL1vkHD_DuB2uQGyg24P_KvAK-A,12024
17
+ wcgw/relay/serve.py,sha256=CYY0mAAzR6nXkdGqLA9dXkgBcMCKPXEAmBcDyutUnjQ,8769
18
18
  wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
19
19
  mcp_wcgw/__init__.py,sha256=fKCgOdN7cn7gR3YGFaGyV5Goe8A2sEyllLcsRkN0i-g,2601
20
20
  mcp_wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -38,7 +38,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
38
38
  mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
39
39
  mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
40
40
  mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
41
- wcgw-2.4.3.dist-info/METADATA,sha256=dwZvKTDJRrxkrx5vmL_QxLQCVhHD75dD8kZhr1PL02M,7931
42
- wcgw-2.4.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- wcgw-2.4.3.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
44
- wcgw-2.4.3.dist-info/RECORD,,
41
+ wcgw-2.5.0.dist-info/METADATA,sha256=D16uxd1XiajjITvdEIdE2b4MI9gYepkor39zOkOQvFI,7924
42
+ wcgw-2.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ wcgw-2.5.0.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
44
+ wcgw-2.5.0.dist-info/licenses/LICENSE,sha256=SDdQZzFz8ehsr0m87ZmC3LL82leZdULi2yJ-XGGuqac,13417
45
+ wcgw-2.5.0.dist-info/RECORD,,
@@ -0,0 +1,243 @@
1
+ The majority of this software is licensed under the MIT License.
2
+ Portions of this software include code licensed under the Apache License, Version 2.0.
3
+
4
+ --------------------------------------------------------------------------------
5
+ MIT License (Primary License)
6
+ --------------------------------------------------------------------------------
7
+
8
+ Copyright (c) 2024 arusia
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ --------------------------------------------------------------------------------
29
+ Third Party Components
30
+ --------------------------------------------------------------------------------
31
+
32
+ This project includes components from the following third-party projects:
33
+
34
+ 1. The file at src/wcgw/client/diff-instructions.txt is substantially modified from Aider
35
+ Source: https://github.com/Aider-AI/aider
36
+ Original Copyright (c) 2023 Aider-AI
37
+ Licensed under the Apache License, Version 2.0
38
+ Modified version Copyright (c) 2024 arusia
39
+
40
+ The full text of the Apache License 2.0 follows:
41
+
42
+ --------------------------------------------------------------------------------
43
+ Apache License
44
+ Version 2.0, January 2004
45
+ http://www.apache.org/licenses/
46
+
47
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
48
+
49
+ 1. Definitions.
50
+
51
+ "License" shall mean the terms and conditions for use, reproduction,
52
+ and distribution as defined by Sections 1 through 9 of this document.
53
+
54
+ "Licensor" shall mean the copyright owner or entity authorized by
55
+ the copyright owner that is granting the License.
56
+
57
+ "Legal Entity" shall mean the union of the acting entity and all
58
+ other entities that control, are controlled by, or are under common
59
+ control with that entity. For the purposes of this definition,
60
+ "control" means (i) the power, direct or indirect, to cause the
61
+ direction or management of such entity, whether by contract or
62
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
63
+ outstanding shares, or (iii) beneficial ownership of such entity.
64
+
65
+ "You" (or "Your") shall mean an individual or Legal Entity
66
+ exercising permissions granted by this License.
67
+
68
+ "Source" form shall mean the preferred form for making modifications,
69
+ including but not limited to software source code, documentation
70
+ source, and configuration files.
71
+
72
+ "Object" form shall mean any form resulting from mechanical
73
+ transformation or translation of a Source form, including but
74
+ not limited to compiled object code, generated documentation,
75
+ and conversions to other media types.
76
+
77
+ "Work" shall mean the work of authorship, whether in Source or
78
+ Object form, made available under the License, as indicated by a
79
+ copyright notice that is included in or attached to the work
80
+ (an example is provided in the Appendix below).
81
+
82
+ "Derivative Works" shall mean any work, whether in Source or Object
83
+ form, that is based on (or derived from) the Work and for which the
84
+ editorial revisions, annotations, elaborations, or other modifications
85
+ represent, as a whole, an original work of authorship. For the purposes
86
+ of this License, Derivative Works shall not include works that remain
87
+ separable from, or merely link (or bind by name) to the interfaces of,
88
+ the Work and Derivative Works thereof.
89
+
90
+ "Contribution" shall mean any work of authorship, including
91
+ the original version of the Work and any modifications or additions
92
+ to that Work or Derivative Works thereof, that is intentionally
93
+ submitted to Licensor for inclusion in the Work by the copyright owner
94
+ or by an individual or Legal Entity authorized to submit on behalf of
95
+ the copyright owner. For the purposes of this definition, "submitted"
96
+ means any form of electronic, verbal, or written communication sent
97
+ to the Licensor or its representatives, including but not limited to
98
+ communication on electronic mailing lists, source code control systems,
99
+ and issue tracking systems that are managed by, or on behalf of, the
100
+ Licensor for the purpose of discussing and improving the Work, but
101
+ excluding communication that is conspicuously marked or otherwise
102
+ designated in writing by the copyright owner as "Not a Contribution."
103
+
104
+ "Contributor" shall mean Licensor and any individual or Legal Entity
105
+ on behalf of whom a Contribution has been received by Licensor and
106
+ subsequently incorporated within the Work.
107
+
108
+ 2. Grant of Copyright License. Subject to the terms and conditions of
109
+ this License, each Contributor hereby grants to You a perpetual,
110
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
111
+ copyright license to reproduce, prepare Derivative Works of,
112
+ publicly display, publicly perform, sublicense, and distribute the
113
+ Work and such Derivative Works in Source or Object form.
114
+
115
+ 3. Grant of Patent License. Subject to the terms and conditions of
116
+ this License, each Contributor hereby grants to You a perpetual,
117
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
118
+ (except as stated in this section) patent license to make, have made,
119
+ use, offer to sell, sell, import, and otherwise transfer the Work,
120
+ where such license applies only to those patent claims licensable
121
+ by such Contributor that are necessarily infringed by their
122
+ Contribution(s) alone or by combination of their Contribution(s)
123
+ with the Work to which such Contribution(s) was submitted. If You
124
+ institute patent litigation against any entity (including a
125
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
126
+ or a Contribution incorporated within the Work constitutes direct
127
+ or contributory patent infringement, then any patent licenses
128
+ granted to You under this License for that Work shall terminate
129
+ as of the date such litigation is filed.
130
+
131
+ 4. Redistribution. You may reproduce and distribute copies of the
132
+ Work or Derivative Works thereof in any medium, with or without
133
+ modifications, and in Source or Object form, provided that You
134
+ meet the following conditions:
135
+
136
+ (a) You must give any other recipients of the Work or
137
+ Derivative Works a copy of this License; and
138
+
139
+ (b) You must cause any modified files to carry prominent notices
140
+ stating that You changed the files; and
141
+
142
+ (c) You must retain, in the Source form of any Derivative Works
143
+ that You distribute, all copyright, patent, trademark, and
144
+ attribution notices from the Source form of the Work,
145
+ excluding those notices that do not pertain to any part of
146
+ the Derivative Works; and
147
+
148
+ (d) If the Work includes a "NOTICE" text file as part of its
149
+ distribution, then any Derivative Works that You distribute must
150
+ include a readable copy of the attribution notices contained
151
+ within such NOTICE file, excluding those notices that do not
152
+ pertain to any part of the Derivative Works, in at least one
153
+ of the following places: within a NOTICE text file distributed
154
+ as part of the Derivative Works; within the Source form or
155
+ documentation, if provided along with the Derivative Works; or,
156
+ within a display generated by the Derivative Works, if and
157
+ wherever such third-party notices normally appear. The contents
158
+ of the NOTICE file are for informational purposes only and
159
+ do not modify the License. You may add Your own attribution
160
+ notices within Derivative Works that You distribute, alongside
161
+ or as an addendum to the NOTICE text from the Work, provided
162
+ that such additional attribution notices cannot be construed
163
+ as modifying the License.
164
+
165
+ You may add Your own copyright statement to Your modifications and
166
+ may provide additional or different license terms and conditions
167
+ for use, reproduction, or distribution of Your modifications, or
168
+ for any such Derivative Works as a whole, provided Your use,
169
+ reproduction, and distribution of the Work otherwise complies with
170
+ the conditions stated in this License.
171
+
172
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
173
+ any Contribution intentionally submitted for inclusion in the Work
174
+ by You to the Licensor shall be under the terms and conditions of
175
+ this License, without any additional terms or conditions.
176
+ Notwithstanding the above, nothing herein shall supersede or modify
177
+ the terms of any separate license agreement you may have executed
178
+ with Licensor regarding such Contributions.
179
+
180
+ 6. Trademarks. This License does not grant permission to use the trade
181
+ names, trademarks, service marks, or product names of the Licensor,
182
+ except as required for reasonable and customary use in describing the
183
+ origin of the Work and reproducing the content of the NOTICE file.
184
+
185
+ 7. Disclaimer of Warranty. Unless required by applicable law or
186
+ agreed to in writing, Licensor provides the Work (and each
187
+ Contributor provides its Contributions) on an "AS IS" BASIS,
188
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
189
+ implied, including, without limitation, any warranties or conditions
190
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
191
+ PARTICULAR PURPOSE. You are solely responsible for determining the
192
+ appropriateness of using or redistributing the Work and assume any
193
+ risks associated with Your exercise of permissions under this License.
194
+
195
+ 8. Limitation of Liability. In no event and under no legal theory,
196
+ whether in tort (including negligence), contract, or otherwise,
197
+ unless required by applicable law (such as deliberate and grossly
198
+ negligent acts) or agreed to in writing, shall any Contributor be
199
+ liable to You for damages, including any direct, indirect, special,
200
+ incidental, or consequential damages of any character arising as a
201
+ result of this License or out of the use or inability to use the
202
+ Work (including but not limited to damages for loss of goodwill,
203
+ work stoppage, computer failure or malfunction, or any and all
204
+ other commercial damages or losses), even if such Contributor
205
+ has been advised of the possibility of such damages.
206
+
207
+ 9. Accepting Warranty or Additional Liability. While redistributing
208
+ the Work or Derivative Works thereof, You may choose to offer,
209
+ and charge a fee for, acceptance of support, warranty, indemnity,
210
+ or other liability obligations and/or rights consistent with this
211
+ License. However, in accepting such obligations, You may act only
212
+ on Your own behalf and on Your sole responsibility, not on behalf
213
+ of any other Contributor, and only if You agree to indemnify,
214
+ defend, and hold each Contributor harmless for any liability
215
+ incurred by, or claims asserted against, such Contributor by reason
216
+ of your accepting any such warranty or additional liability.
217
+
218
+ END OF TERMS AND CONDITIONS
219
+
220
+ APPENDIX: How to apply the Apache License to your work.
221
+
222
+ To apply the Apache License to your work, attach the following
223
+ boilerplate notice, with the fields enclosed by brackets "[]"
224
+ replaced with your own identifying information. (Don't include
225
+ the brackets!) The text should be enclosed in the appropriate
226
+ comment syntax for the file format. We also recommend that a
227
+ file or class name and description of purpose be included on the
228
+ same "printed page" as the copyright notice for easier
229
+ identification within third-party archives.
230
+
231
+ Copyright [yyyy] [name of copyright owner]
232
+
233
+ Licensed under the Apache License, Version 2.0 (the "License");
234
+ you may not use this file except in compliance with the License.
235
+ You may obtain a copy of the License at
236
+
237
+ http://www.apache.org/licenses/LICENSE-2.0
238
+
239
+ Unless required by applicable law or agreed to in writing, software
240
+ distributed under the License is distributed on an "AS IS" BASIS,
241
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
242
+ See the License for the specific language governing permissions and
243
+ limitations under the License.
File without changes