wcgw 0.1.2__py3-none-any.whl → 1.0.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 CHANGED
@@ -1,2 +1,2 @@
1
- from .basic import app, loop
2
- from .tools import run as listen
1
+ from .client.basic import app, loop
2
+ from .client.tools import run as listen
@@ -0,0 +1,3 @@
1
+ from .tools import run
2
+
3
+ run()
@@ -20,16 +20,15 @@ import petname # type: ignore[import-untyped]
20
20
  from typer import Typer
21
21
  import uuid
22
22
 
23
+ from ..types_ import BashCommand, BashInteraction, ReadImage, Writefile, ResetShell
24
+
23
25
  from .common import Models, discard_input
24
26
  from .common import CostData, History
25
27
  from .openai_utils import get_input_cost, get_output_cost
26
- from .tools import ExecuteBash, ReadImage, ImageData
28
+ from .tools import ImageData
27
29
 
28
30
  from .tools import (
29
- BASH_CLF_OUTPUT,
30
- Confirmation,
31
31
  DoneFlag,
32
- Writefile,
33
32
  get_tool_output,
34
33
  SHELL,
35
34
  start_shell,
@@ -156,26 +155,32 @@ def loop(
156
155
 
157
156
  tools = [
158
157
  openai.pydantic_function_tool(
159
- ExecuteBash,
158
+ BashCommand,
160
159
  description="""
161
- - Execute a bash script. This is stateful (beware with subsequent calls).
162
- - Execute commands using `execute_command` attribute.
160
+ - Execute a bash command. This is stateful (beware with subsequent calls).
163
161
  - Do not use interactive commands like nano. Prefer writing simpler commands.
164
- - Last line will always be `(exit <int code>)` except if
165
- - The last line is `(pending)` if the program is still running or waiting for your input. You can then send input using `send_ascii` attributes. You get status by sending new line `send_ascii: ["Enter"]` or `send_ascii: [10]`.
166
- - Optionally the last line is `(won't exit)` in which case you need to kill the process if you want to run a new command.
162
+ - Status of the command and the current working directory will always be returned at the end.
167
163
  - Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
168
164
  - The first line might be `(...truncated)` if the output is too long.
169
165
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
170
166
  """,
167
+ ),
168
+ openai.pydantic_function_tool(
169
+ BashInteraction,
170
+ description="""
171
+ - Interact with running program using this tool.""",
171
172
  ),
172
173
  openai.pydantic_function_tool(
173
174
  Writefile,
174
- description="Write content to a file. Provide file path and content. Use this instead of ExecuteBash for writing files.",
175
+ description="Write content to a file. Provide file path and content. Use this instead of BashCommand for writing files.",
175
176
  ),
176
177
  openai.pydantic_function_tool(
177
178
  ReadImage, description="Read an image from the shell."
178
179
  ),
180
+ openai.pydantic_function_tool(
181
+ ResetShell,
182
+ description="Resets the shell. Use only if all interrupts and prompt reset attempts have failed repeatedly.",
183
+ ),
179
184
  ]
180
185
  uname_sysname = os.uname().sysname
181
186
  uname_machine = os.uname().machine
@@ -2,8 +2,10 @@ import asyncio
2
2
  import base64
3
3
  import json
4
4
  import mimetypes
5
+ import re
5
6
  import sys
6
7
  import threading
8
+ import importlib.metadata
7
9
  import traceback
8
10
  from typing import (
9
11
  Callable,
@@ -11,12 +13,12 @@ from typing import (
11
13
  NewType,
12
14
  Optional,
13
15
  ParamSpec,
14
- Sequence,
15
16
  TypeVar,
16
17
  TypedDict,
17
18
  )
18
19
  import uuid
19
20
  from pydantic import BaseModel, TypeAdapter
21
+ import typer
20
22
  from websockets.sync.client import connect as syncconnect
21
23
 
22
24
  import os
@@ -38,6 +40,14 @@ from openai.types.chat import (
38
40
  ChatCompletionMessage,
39
41
  ParsedChatCompletionMessage,
40
42
  )
43
+ from nltk.metrics.distance import edit_distance
44
+ from ..types_ import FileEditFindReplace, ResetShell, Writefile
45
+
46
+ from ..types_ import BashCommand
47
+
48
+ from ..types_ import BashInteraction
49
+
50
+ from ..types_ import ReadImage
41
51
 
42
52
  from .common import CostData, Models, discard_input
43
53
 
@@ -72,22 +82,20 @@ def ask_confirmation(prompt: Confirmation) -> str:
72
82
  return "Yes" if response.lower() == "y" else "No"
73
83
 
74
84
 
75
- class Writefile(BaseModel):
76
- file_path: str
77
- file_content: str
85
+ PROMPT = "#@@"
78
86
 
79
87
 
80
- def start_shell() -> pexpect.spawn:
88
+ def start_shell() -> pexpect.spawn: # type: ignore
81
89
  SHELL = pexpect.spawn(
82
90
  "/bin/bash --noprofile --norc",
83
- env={**os.environ, **{"PS1": "#@@"}}, # type: ignore[arg-type]
91
+ env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
84
92
  echo=False,
85
93
  encoding="utf-8",
86
94
  timeout=TIMEOUT,
87
95
  )
88
- SHELL.expect("#@@")
96
+ SHELL.expect(PROMPT)
89
97
  SHELL.sendline("stty -icanon -echo")
90
- SHELL.expect("#@@")
98
+ SHELL.expect(PROMPT)
91
99
  return SHELL
92
100
 
93
101
 
@@ -103,16 +111,22 @@ def _is_int(mystr: str) -> bool:
103
111
 
104
112
 
105
113
  def _get_exit_code() -> int:
114
+ if PROMPT != "#@@":
115
+ return 0
106
116
  # First reset the prompt in case venv was sourced or other reasons.
107
- SHELL.sendline('export PS1="#@@"')
108
- SHELL.expect("#@@")
117
+ SHELL.sendline(f"export PS1={PROMPT}")
118
+ SHELL.expect(PROMPT)
109
119
  # Reset echo also if it was enabled
110
120
  SHELL.sendline("stty -icanon -echo")
111
- SHELL.expect("#@@")
121
+ SHELL.expect(PROMPT)
112
122
  SHELL.sendline("echo $?")
113
123
  before = ""
114
124
  while not _is_int(before): # Consume all previous output
115
- SHELL.expect("#@@")
125
+ try:
126
+ SHELL.expect(PROMPT)
127
+ except pexpect.TIMEOUT:
128
+ print(f"Couldn't get exit code, before: {before}")
129
+ raise
116
130
  assert isinstance(SHELL.before, str)
117
131
  # Render because there could be some anscii escape sequences still set like in google colab env
118
132
  before = render_terminal_output(SHELL.before).strip()
@@ -123,51 +137,110 @@ def _get_exit_code() -> int:
123
137
  raise ValueError(f"Malformed output: {before}")
124
138
 
125
139
 
126
- Specials = Literal["Key-up", "Key-down", "Key-left", "Key-right", "Enter", "Ctrl-c"]
140
+ BASH_CLF_OUTPUT = Literal["repl", "pending"]
141
+ BASH_STATE: BASH_CLF_OUTPUT = "repl"
142
+ CWD = os.getcwd()
143
+
144
+
145
+ def reset_shell() -> str:
146
+ global SHELL, BASH_STATE, CWD
147
+ SHELL.close(True)
148
+ SHELL = start_shell()
149
+ BASH_STATE = "repl"
150
+ CWD = os.getcwd()
151
+ return "Reset successful" + get_status()
152
+
153
+
154
+ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
155
+ 1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
156
+ 2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
157
+ 3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
158
+ 4. Send the process in background using `send_specials: ["Ctrl-z"]` followed by BashCommand: `bg`
159
+ """
160
+
161
+
162
+ def update_repl_prompt(command: str) -> bool:
163
+ global PROMPT
164
+ if re.match(r"^wcgw_update_prompt\(\)$", command.strip()):
165
+ SHELL.sendintr()
166
+ index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
167
+ if index == 0:
168
+ return False
169
+ before = SHELL.before or ""
170
+ assert before, "Something went wrong updating repl prompt"
171
+ PROMPT = before.split("\n")[-1].strip()
172
+ # Escape all regex
173
+ PROMPT = re.escape(PROMPT)
174
+ print(f"Trying to update prompt to: {PROMPT.encode()!r}")
175
+ index = 0
176
+ while index == 0:
177
+ # Consume all REPL prompts till now
178
+ index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
179
+ print(f"Prompt updated to: {PROMPT}")
180
+ return True
181
+ return False
127
182
 
128
183
 
129
- class ExecuteBash(BaseModel):
130
- execute_command: Optional[str] = None
131
- send_ascii: Optional[Sequence[int | Specials]] = None
184
+ def get_cwd() -> str:
185
+ SHELL.sendline("pwd")
186
+ SHELL.expect(PROMPT)
187
+ assert isinstance(SHELL.before, str)
188
+ current_dir = render_terminal_output(SHELL.before).strip()
189
+ return current_dir
132
190
 
133
191
 
134
- BASH_CLF_OUTPUT = Literal["running", "waiting_for_input", "wont_exit"]
135
- BASH_STATE: BASH_CLF_OUTPUT = "running"
192
+ def get_status() -> str:
193
+ global CWD
194
+ exit_code: Optional[int] = None
136
195
 
196
+ status = "\n\n---\n\n"
197
+ if BASH_STATE == "pending":
198
+ status += "status = still running\n"
199
+ status += "cwd = " + CWD + "\n"
200
+ else:
201
+ exit_code = _get_exit_code()
202
+ status += f"status = exited with code {exit_code}\n"
203
+ CWD = get_cwd()
204
+ status += "cwd = " + CWD + "\n"
137
205
 
138
- WAITING_INPUT_MESSAGE = """A command is already running waiting for input. NOTE: You can't run multiple shell sessions, likely a previous program hasn't exited.
139
- 1. Get its output using `send_ascii: [10]`
140
- 2. Use `send_ascii` to give inputs to the running program, don't use `execute_command` OR
141
- 3. kill the previous program by sending ctrl+c first using `send_ascii`"""
206
+ return status.rstrip()
142
207
 
143
208
 
144
209
  def execute_bash(
145
- enc: tiktoken.Encoding, bash_arg: ExecuteBash, max_tokens: Optional[int]
210
+ enc: tiktoken.Encoding,
211
+ bash_arg: BashCommand | BashInteraction,
212
+ max_tokens: Optional[int],
146
213
  ) -> tuple[str, float]:
147
- global SHELL, BASH_STATE
214
+ global SHELL, BASH_STATE, CWD
148
215
  try:
149
- if bash_arg.execute_command:
150
- if BASH_STATE == "waiting_for_input":
151
- raise ValueError(WAITING_INPUT_MESSAGE)
152
- elif BASH_STATE == "wont_exit":
153
- raise ValueError(
154
- """A command is already running that hasn't exited. NOTE: You can't run multiple shell sessions, likely a previous program is in infinite loop.
155
- Kill the previous program by sending ctrl+c first using `send_ascii`"""
216
+ is_interrupt = False
217
+ if isinstance(bash_arg, BashCommand):
218
+ updated_repl_mode = update_repl_prompt(bash_arg.command)
219
+ if updated_repl_mode:
220
+ BASH_STATE = "repl"
221
+ response = (
222
+ "Prompt updated, you can execute REPL lines using BashCommand now"
156
223
  )
157
- command = bash_arg.execute_command.strip()
224
+ console.print(response)
225
+ return (
226
+ response,
227
+ 0,
228
+ )
229
+
230
+ console.print(f"$ {bash_arg.command}")
231
+ if BASH_STATE == "pending":
232
+ raise ValueError(WAITING_INPUT_MESSAGE)
233
+ command = bash_arg.command.strip()
158
234
 
159
235
  if "\n" in command:
160
236
  raise ValueError(
161
237
  "Command should not contain newline character in middle. Run only one command at a time."
162
238
  )
163
239
 
164
- console.print(f"$ {command}")
165
240
  SHELL.sendline(command)
166
- elif bash_arg.send_ascii:
167
- console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
168
- for char in bash_arg.send_ascii:
169
- if isinstance(char, int):
170
- SHELL.send(chr(char))
241
+ elif bash_arg.send_specials:
242
+ console.print(f"Sending special sequence: {bash_arg.send_specials}")
243
+ for char in bash_arg.send_specials:
171
244
  if char == "Key-up":
172
245
  SHELL.send("\033[A")
173
246
  elif char == "Key-down":
@@ -180,26 +253,53 @@ def execute_bash(
180
253
  SHELL.send("\n")
181
254
  elif char == "Ctrl-c":
182
255
  SHELL.sendintr()
256
+ is_interrupt = True
257
+ elif char == "Ctrl-d":
258
+ SHELL.sendintr()
259
+ is_interrupt = True
260
+ elif char == "Ctrl-z":
261
+ SHELL.send("\x1a")
262
+ else:
263
+ raise Exception(f"Unknown special character: {char}")
264
+ elif bash_arg.send_ascii:
265
+ console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
266
+ for ascii_char in bash_arg.send_ascii:
267
+ SHELL.send(chr(ascii_char))
268
+ if ascii_char == 3:
269
+ is_interrupt = True
183
270
  else:
184
- raise Exception("Nothing to send")
185
- BASH_STATE = "running"
271
+ if bash_arg.send_text is None:
272
+ return (
273
+ "Failure: at least one of send_text, send_specials or send_ascii should be provided",
274
+ 0.0,
275
+ )
276
+
277
+ updated_repl_mode = update_repl_prompt(bash_arg.send_text)
278
+ if updated_repl_mode:
279
+ BASH_STATE = "repl"
280
+ response = (
281
+ "Prompt updated, you can execute REPL lines using BashCommand now"
282
+ )
283
+ console.print(response)
284
+ return (
285
+ response,
286
+ 0,
287
+ )
288
+ console.print(f"Interact text: {bash_arg.send_text}")
289
+ SHELL.sendline(bash_arg.send_text)
290
+
291
+ BASH_STATE = "repl"
186
292
 
187
293
  except KeyboardInterrupt:
188
- SHELL.close(True)
189
- SHELL = start_shell()
190
- raise
294
+ SHELL.sendintr()
295
+ SHELL.expect(PROMPT)
296
+ return "---\n\nFailure: user interrupted the execution", 0.0
191
297
 
192
298
  wait = 5
193
- index = SHELL.expect(["#@@", pexpect.TIMEOUT], timeout=wait)
194
- running = ""
195
- while index == 1:
196
- if wait > TIMEOUT:
197
- raise TimeoutError("Timeout while waiting for shell prompt")
198
-
199
- BASH_STATE = "waiting_for_input"
299
+ index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=wait)
300
+ if index == 1:
301
+ BASH_STATE = "pending"
200
302
  text = SHELL.before or ""
201
- print(text[len(running) :])
202
- running = text
203
303
 
204
304
  text = render_terminal_output(text)
205
305
  tokens = enc.encode(text)
@@ -207,8 +307,25 @@ def execute_bash(
207
307
  if max_tokens and len(tokens) >= max_tokens:
208
308
  text = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
209
309
 
210
- last_line = "(pending)"
211
- return text + f"\n{last_line}", 0
310
+ if is_interrupt:
311
+ text = (
312
+ text
313
+ + """---
314
+ ----
315
+ Failure interrupting.
316
+ If any REPL session was previously running or if bashrc was sourced, or if there is issue to other REPL related reasons:
317
+ Run BashCommand: "wcgw_update_prompt()" to reset the PS1 prompt.
318
+ Otherwise, you may want to try Ctrl-c again or program specific exit interactive commands.
319
+ """
320
+ )
321
+
322
+ exit_status = get_status()
323
+ text += exit_status
324
+
325
+ return text, 0
326
+
327
+ if is_interrupt:
328
+ return "Interrupt successful", 0.0
212
329
 
213
330
  assert isinstance(SHELL.before, str)
214
331
  output = render_terminal_output(SHELL.before)
@@ -218,9 +335,8 @@ def execute_bash(
218
335
  output = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
219
336
 
220
337
  try:
221
- exit_code = _get_exit_code()
222
- output += f"\n(exit {exit_code})"
223
-
338
+ exit_status = get_status()
339
+ output += exit_status
224
340
  except ValueError as e:
225
341
  console.print(output)
226
342
  traceback.print_exc()
@@ -232,11 +348,6 @@ def execute_bash(
232
348
  return output, 0
233
349
 
234
350
 
235
- class ReadImage(BaseModel):
236
- file_path: str
237
- type: Literal["ReadImage"] = "ReadImage"
238
-
239
-
240
351
  def serve_image_in_bg(file_path: str, client_uuid: str, name: str) -> None:
241
352
  if not client_uuid:
242
353
  client_uuid = str(uuid.uuid4())
@@ -269,25 +380,17 @@ T = TypeVar("T")
269
380
  def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
270
381
  def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
271
382
  global BASH_STATE
272
- if BASH_STATE == "waiting_for_input":
383
+ if BASH_STATE == "pending":
273
384
  raise ValueError(WAITING_INPUT_MESSAGE)
274
- elif BASH_STATE == "wont_exit":
275
- raise ValueError(
276
- "A command is already running that hasn't exited. NOTE: You can't run multiple shell sessions, likely the previous program is in infinite loop. Please kill the previous program by sending ctrl+c first."
277
- )
385
+
278
386
  return func(*args, **kwargs)
279
387
 
280
388
  return wrapper
281
389
 
282
390
 
283
- @ensure_no_previous_output
284
391
  def read_image_from_shell(file_path: str) -> ImageData:
285
392
  if not os.path.isabs(file_path):
286
- SHELL.sendline("pwd")
287
- SHELL.expect("#@@")
288
- assert isinstance(SHELL.before, str)
289
- current_dir = render_terminal_output(SHELL.before).strip()
290
- file_path = os.path.join(current_dir, file_path)
393
+ file_path = os.path.join(CWD, file_path)
291
394
 
292
395
  if not os.path.exists(file_path):
293
396
  raise ValueError(f"File {file_path} does not exist")
@@ -299,22 +402,65 @@ def read_image_from_shell(file_path: str) -> ImageData:
299
402
  return ImageData(dataurl=f"data:{image_type};base64,{image_b64}")
300
403
 
301
404
 
302
- @ensure_no_previous_output
303
405
  def write_file(writefile: Writefile) -> str:
304
406
  if not os.path.isabs(writefile.file_path):
305
- SHELL.sendline("pwd")
306
- SHELL.expect("#@@")
307
- assert isinstance(SHELL.before, str)
308
- current_dir = render_terminal_output(SHELL.before).strip()
309
- return f"Failure: Use absolute path only. FYI current working directory is '{current_dir}'"
310
- os.makedirs(os.path.dirname(writefile.file_path), exist_ok=True)
407
+ path_ = os.path.join(CWD, writefile.file_path)
408
+ else:
409
+ path_ = writefile.file_path
311
410
  try:
312
- with open(writefile.file_path, "w") as f:
411
+ with open(path_, "w") as f:
313
412
  f.write(writefile.file_content)
314
413
  except OSError as e:
315
- console.print(f"Error: {e}", style="red")
316
414
  return f"Error: {e}"
317
- console.print(f"File written to {writefile.file_path}")
415
+ console.print(f"File written to {path_}")
416
+ return "Success"
417
+
418
+
419
+ def find_least_edit_distance_substring(content: str, find_str: str) -> str:
420
+ content_lines = content.split("\n")
421
+ find_lines = find_str.split("\n")
422
+ # Slide window and find one with sum of edit distance least
423
+ min_edit_distance = float("inf")
424
+ min_edit_distance_lines = []
425
+ for i in range(len(content_lines) - len(find_lines) + 1):
426
+ edit_distance_sum = 0
427
+ for j in range(len(find_lines)):
428
+ edit_distance_sum += edit_distance(content_lines[i + j], find_lines[j])
429
+ if edit_distance_sum < min_edit_distance:
430
+ min_edit_distance = edit_distance_sum
431
+ min_edit_distance_lines = content_lines[i : i + len(find_lines)]
432
+ return "\n".join(min_edit_distance_lines)
433
+
434
+
435
+ def file_edit(file_edit: FileEditFindReplace) -> str:
436
+ if not os.path.isabs(file_edit.file_path):
437
+ path_ = os.path.join(CWD, file_edit.file_path)
438
+ else:
439
+ path_ = file_edit.file_path
440
+
441
+ out_string = "\n".join("> " + line for line in file_edit.find_lines.split("\n"))
442
+ in_string = "\n".join(
443
+ "< " + line for line in file_edit.replace_with_lines.split("\n")
444
+ )
445
+ console.log(f"Editing file: {path_}\n---\n{out_string}\n---\n{in_string}\n---")
446
+ try:
447
+ with open(path_) as f:
448
+ content = f.read()
449
+ # First find counts
450
+ count = content.count(file_edit.find_lines)
451
+
452
+ if count == 0:
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"
457
+
458
+ content = content.replace(file_edit.find_lines, file_edit.replace_with_lines)
459
+ with open(path_, "w") as f:
460
+ f.write(content)
461
+ except OSError as e:
462
+ return f"Error: {e}"
463
+ console.print(f"File written to {path_}")
318
464
  return "Success"
319
465
 
320
466
 
@@ -342,16 +488,37 @@ def take_help_of_ai_assistant(
342
488
 
343
489
  def which_tool(args: str) -> BaseModel:
344
490
  adapter = TypeAdapter[
345
- Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage
346
- ](Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage)
491
+ Confirmation
492
+ | BashCommand
493
+ | BashInteraction
494
+ | ResetShell
495
+ | Writefile
496
+ | FileEditFindReplace
497
+ | AIAssistant
498
+ | DoneFlag
499
+ | ReadImage
500
+ ](
501
+ Confirmation
502
+ | BashCommand
503
+ | BashInteraction
504
+ | ResetShell
505
+ | Writefile
506
+ | FileEditFindReplace
507
+ | AIAssistant
508
+ | DoneFlag
509
+ | ReadImage
510
+ )
347
511
  return adapter.validate_python(json.loads(args))
348
512
 
349
513
 
350
514
  def get_tool_output(
351
515
  args: dict[object, object]
352
516
  | Confirmation
353
- | ExecuteBash
517
+ | BashCommand
518
+ | BashInteraction
519
+ | ResetShell
354
520
  | Writefile
521
+ | FileEditFindReplace
355
522
  | AIAssistant
356
523
  | DoneFlag
357
524
  | ReadImage,
@@ -362,8 +529,26 @@ def get_tool_output(
362
529
  ) -> tuple[str | ImageData | DoneFlag, float]:
363
530
  if isinstance(args, dict):
364
531
  adapter = TypeAdapter[
365
- Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage
366
- ](Confirmation | ExecuteBash | Writefile | AIAssistant | DoneFlag | ReadImage)
532
+ Confirmation
533
+ | BashCommand
534
+ | BashInteraction
535
+ | ResetShell
536
+ | Writefile
537
+ | FileEditFindReplace
538
+ | AIAssistant
539
+ | DoneFlag
540
+ | ReadImage
541
+ ](
542
+ Confirmation
543
+ | BashCommand
544
+ | BashInteraction
545
+ | ResetShell
546
+ | Writefile
547
+ | FileEditFindReplace
548
+ | AIAssistant
549
+ | DoneFlag
550
+ | ReadImage
551
+ )
367
552
  arg = adapter.validate_python(args)
368
553
  else:
369
554
  arg = args
@@ -371,12 +556,15 @@ def get_tool_output(
371
556
  if isinstance(arg, Confirmation):
372
557
  console.print("Calling ask confirmation tool")
373
558
  output = ask_confirmation(arg), 0.0
374
- elif isinstance(arg, ExecuteBash):
559
+ elif isinstance(arg, (BashCommand | BashInteraction)):
375
560
  console.print("Calling execute bash tool")
376
561
  output = execute_bash(enc, arg, max_tokens)
377
562
  elif isinstance(arg, Writefile):
378
563
  console.print("Calling write file tool")
379
564
  output = write_file(arg), 0
565
+ elif isinstance(arg, FileEditFindReplace):
566
+ console.print("Calling file edit tool")
567
+ output = file_edit(arg), 0.0
380
568
  elif isinstance(arg, DoneFlag):
381
569
  console.print("Calling mark finish tool")
382
570
  output = mark_finish(arg), 0.0
@@ -386,6 +574,9 @@ def get_tool_output(
386
574
  elif isinstance(arg, ReadImage):
387
575
  console.print("Calling read image tool")
388
576
  output = read_image_from_shell(arg.file_path), 0.0
577
+ elif isinstance(arg, ResetShell):
578
+ console.print("Calling reset shell tool")
579
+ output = reset_shell(), 0.0
389
580
  else:
390
581
  raise ValueError(f"Unknown tool: {arg}")
391
582
 
@@ -395,44 +586,6 @@ def get_tool_output(
395
586
 
396
587
  History = list[ChatCompletionMessageParam]
397
588
 
398
-
399
- def get_is_waiting_user_input(
400
- model: Models, cost_data: CostData
401
- ) -> Callable[[str], tuple[BASH_CLF_OUTPUT, float]]:
402
- enc = tiktoken.encoding_for_model(model if not model.startswith("o1") else "gpt-4o")
403
- system_prompt = """You need to classify if a bash program is waiting for user input based on its stdout, or if it won't exit. You'll be given the output of any program.
404
- Return `waiting_for_input` if the program is waiting for INTERACTIVE input only, Return 'running' if it's waiting for external resources or just waiting to finish.
405
- Return `wont_exit` if the program won't exit, for example if it's a server.
406
- Return `running` otherwise.
407
- """
408
- history: History = [{"role": "system", "content": system_prompt}]
409
- client = OpenAI()
410
-
411
- class ExpectedOutput(BaseModel):
412
- output_classified: BASH_CLF_OUTPUT
413
-
414
- def is_waiting_user_input(output: str) -> tuple[BASH_CLF_OUTPUT, float]:
415
- # Send only last 30 lines
416
- output = "\n".join(output.split("\n")[-30:])
417
- # Send only max last 200 tokens
418
- output = enc.decode(enc.encode(output)[-200:])
419
-
420
- history.append({"role": "user", "content": output})
421
- response = client.beta.chat.completions.parse(
422
- model=model, messages=history, response_format=ExpectedOutput
423
- )
424
- parsed = response.choices[0].message.parsed
425
- if parsed is None:
426
- raise ValueError("No parsed output")
427
- cost = (
428
- get_input_cost(cost_data, enc, history)[0]
429
- + get_output_cost(cost_data, enc, response.choices[0].message)[0]
430
- )
431
- return parsed.output_classified, cost
432
-
433
- return is_waiting_user_input
434
-
435
-
436
589
  default_enc = tiktoken.encoding_for_model("gpt-4o")
437
590
  default_model: Models = "gpt-4o-2024-08-06"
438
591
  default_cost = CostData(cost_per_1m_input_tokens=0.15, cost_per_1m_output_tokens=0.6)
@@ -440,7 +593,7 @@ curr_cost = 0.0
440
593
 
441
594
 
442
595
  class Mdata(BaseModel):
443
- data: ExecuteBash | Writefile
596
+ data: BashCommand | BashInteraction | Writefile | ResetShell | FileEditFindReplace
444
597
 
445
598
 
446
599
  execution_lock = threading.Lock()
@@ -455,7 +608,7 @@ def execute_user_input() -> None:
455
608
  console.log(
456
609
  execute_bash(
457
610
  default_enc,
458
- ExecuteBash(
611
+ BashInteraction(
459
612
  send_ascii=[ord(x) for x in user_input] + [ord("\n")]
460
613
  ),
461
614
  max_tokens=None,
@@ -474,6 +627,11 @@ async def register_client(server_url: str, client_uuid: str = "") -> None:
474
627
 
475
628
  # Create the WebSocket connection
476
629
  async with websockets.connect(f"{server_url}/{client_uuid}") as websocket:
630
+ server_version = str(await websocket.recv())
631
+ print(f"Server version: {server_version}")
632
+ client_version = importlib.metadata.version("wcgw")
633
+ await websocket.send(client_version)
634
+
477
635
  print(
478
636
  f"Connected. Share this user id with the chatbot: {client_uuid} \nLink: https://chatgpt.com/g/g-Us0AAXkRh-wcgw-giving-shell-access"
479
637
  )
@@ -505,8 +663,15 @@ run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True)
505
663
 
506
664
  @run.command()
507
665
  def app(
508
- server_url: str = "wss://wcgw.arcfu.com/register", client_uuid: Optional[str] = None
666
+ server_url: str = "wss://wcgw.arcfu.com/v1/register",
667
+ client_uuid: Optional[str] = None,
668
+ version: bool = typer.Option(False, "--version", "-v"),
509
669
  ) -> None:
670
+ if version:
671
+ version_ = importlib.metadata.version("wcgw")
672
+ print(f"wcgw version: {version_}")
673
+ exit()
674
+
510
675
  thread1 = threading.Thread(target=execute_user_input)
511
676
  thread2 = threading.Thread(
512
677
  target=asyncio.run, args=(register_client(server_url, client_uuid or ""),)
wcgw/relay/serve.py ADDED
@@ -0,0 +1,326 @@
1
+ import asyncio
2
+ import base64
3
+ from importlib import metadata
4
+ import semantic_version # type: ignore[import-untyped]
5
+ import threading
6
+ import time
7
+ from typing import Any, Callable, Coroutine, DefaultDict, Literal, Optional, Sequence
8
+ from uuid import UUID
9
+ import fastapi
10
+ from fastapi import Response, WebSocket, WebSocketDisconnect
11
+ from pydantic import BaseModel
12
+ import uvicorn
13
+ from fastapi.staticfiles import StaticFiles
14
+
15
+ from dotenv import load_dotenv
16
+
17
+ from ..types_ import (
18
+ BashCommand,
19
+ BashInteraction,
20
+ FileEditFindReplace,
21
+ ResetShell,
22
+ Writefile,
23
+ Specials,
24
+ )
25
+
26
+
27
+ class Mdata(BaseModel):
28
+ data: BashCommand | BashInteraction | Writefile | ResetShell | FileEditFindReplace
29
+ user_id: UUID
30
+
31
+
32
+ app = fastapi.FastAPI()
33
+
34
+ clients: dict[UUID, Callable[[Mdata], Coroutine[None, None, None]]] = {}
35
+ websockets: dict[UUID, WebSocket] = {}
36
+ gpts: dict[UUID, Callable[[str], None]] = {}
37
+
38
+ images: DefaultDict[UUID, dict[str, dict[str, Any]]] = DefaultDict(dict)
39
+
40
+
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
+ CLIENT_VERSION_MINIMUM = "1.0.0"
75
+
76
+
77
+ @app.websocket("/v1/register/{uuid}")
78
+ async def register_websocket(websocket: WebSocket, uuid: UUID) -> None:
79
+ await websocket.accept()
80
+
81
+ # send server version
82
+ version = metadata.version("wcgw")
83
+ await websocket.send_text(version)
84
+
85
+ # receive client version
86
+ client_version = await websocket.receive_text()
87
+ sem_version_client = semantic_version.Version.coerce(client_version)
88
+ sem_version_server = semantic_version.Version.coerce(CLIENT_VERSION_MINIMUM)
89
+ if sem_version_client < sem_version_server:
90
+ await websocket.send_text(
91
+ f"Client version {client_version} is outdated. Please upgrade to {CLIENT_VERSION_MINIMUM} or higher."
92
+ )
93
+ await websocket.close(
94
+ reason="Client version outdated. Please upgrade to the latest version.",
95
+ code=1002,
96
+ )
97
+ return
98
+
99
+ # Register the callback for this client UUID
100
+ async def send_data_callback(data: Mdata) -> None:
101
+ await websocket.send_text(data.model_dump_json())
102
+
103
+ clients[uuid] = send_data_callback
104
+ websockets[uuid] = websocket
105
+
106
+ try:
107
+ while True:
108
+ received_data = await websocket.receive_text()
109
+ if uuid not in gpts:
110
+ raise fastapi.HTTPException(status_code=400, detail="No call made")
111
+ gpts[uuid](received_data)
112
+ except WebSocketDisconnect:
113
+ # Remove the client if the WebSocket is disconnected
114
+ del clients[uuid]
115
+ del websockets[uuid]
116
+ print(f"Client {uuid} disconnected")
117
+
118
+
119
+ @app.post("/write_file")
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):
128
+ user_id: UUID
129
+
130
+
131
+ @app.post("/v1/write_file")
132
+ async def write_file(write_file_data: WritefileWithUUID) -> str:
133
+ user_id = write_file_data.user_id
134
+ if user_id not in clients:
135
+ raise fastapi.HTTPException(
136
+ status_code=404, detail="User with the provided id not found"
137
+ )
138
+
139
+ results: Optional[str] = None
140
+
141
+ def put_results(result: str) -> None:
142
+ nonlocal results
143
+ results = result
144
+
145
+ gpts[user_id] = put_results
146
+
147
+ await clients[user_id](Mdata(data=write_file_data, user_id=user_id))
148
+
149
+ start_time = time.time()
150
+ while time.time() - start_time < 30:
151
+ if results is not None:
152
+ return results
153
+ await asyncio.sleep(0.1)
154
+
155
+ raise fastapi.HTTPException(status_code=500, detail="Timeout error")
156
+
157
+
158
+ class FileEditFindReplaceWithUUID(FileEditFindReplace):
159
+ user_id: UUID
160
+
161
+
162
+ @app.post("/v1/file_edit_find_replace")
163
+ async def file_edit_find_replace(
164
+ file_edit_find_replace: FileEditFindReplaceWithUUID,
165
+ ) -> str:
166
+ user_id = file_edit_find_replace.user_id
167
+ if user_id not in clients:
168
+ raise fastapi.HTTPException(
169
+ status_code=404, detail="User with the provided id not found"
170
+ )
171
+
172
+ results: Optional[str] = None
173
+
174
+ def put_results(result: str) -> None:
175
+ nonlocal results
176
+ results = result
177
+
178
+ gpts[user_id] = put_results
179
+
180
+ await clients[user_id](
181
+ Mdata(
182
+ data=file_edit_find_replace,
183
+ user_id=user_id,
184
+ )
185
+ )
186
+
187
+ start_time = time.time()
188
+ while time.time() - start_time < 30:
189
+ if results is not None:
190
+ return results
191
+ await asyncio.sleep(0.1)
192
+
193
+ raise fastapi.HTTPException(status_code=500, detail="Timeout error")
194
+
195
+
196
+ class ResetShellWithUUID(ResetShell):
197
+ user_id: UUID
198
+
199
+
200
+ @app.post("/v1/reset_shell")
201
+ async def reset_shell(reset_shell: ResetShellWithUUID) -> str:
202
+ user_id = reset_shell.user_id
203
+ if user_id not in clients:
204
+ raise fastapi.HTTPException(
205
+ status_code=404, detail="User with the provided id not found"
206
+ )
207
+
208
+ results: Optional[str] = None
209
+
210
+ def put_results(result: str) -> None:
211
+ nonlocal results
212
+ results = result
213
+
214
+ gpts[user_id] = put_results
215
+
216
+ await clients[user_id](Mdata(data=reset_shell, user_id=user_id))
217
+
218
+ start_time = time.time()
219
+ while time.time() - start_time < 30:
220
+ if results is not None:
221
+ return results
222
+ await asyncio.sleep(0.1)
223
+
224
+ raise fastapi.HTTPException(status_code=500, detail="Timeout error")
225
+
226
+
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
+ class CommandWithUUID(BaseModel):
236
+ command: str
237
+ user_id: UUID
238
+
239
+
240
+ @app.post("/v1/bash_command")
241
+ async def bash_command(command: CommandWithUUID) -> str:
242
+ user_id = command.user_id
243
+ if user_id not in clients:
244
+ raise fastapi.HTTPException(
245
+ status_code=404, detail="User with the provided id not found"
246
+ )
247
+
248
+ results: Optional[str] = None
249
+
250
+ def put_results(result: str) -> None:
251
+ nonlocal results
252
+ results = result
253
+
254
+ gpts[user_id] = put_results
255
+
256
+ await clients[user_id](
257
+ Mdata(data=BashCommand(command=command.command), user_id=user_id)
258
+ )
259
+
260
+ start_time = time.time()
261
+ while time.time() - start_time < 30:
262
+ if results is not None:
263
+ return results
264
+ await asyncio.sleep(0.1)
265
+
266
+ raise fastapi.HTTPException(status_code=500, detail="Timeout error")
267
+
268
+
269
+ class BashInteractionWithUUID(BashInteraction):
270
+ user_id: UUID
271
+
272
+
273
+ @app.post("/v1/bash_interaction")
274
+ async def bash_interaction(bash_interaction: BashInteractionWithUUID) -> str:
275
+ user_id = bash_interaction.user_id
276
+ if user_id not in clients:
277
+ raise fastapi.HTTPException(
278
+ status_code=404, detail="User with the provided id not found"
279
+ )
280
+
281
+ results: Optional[str] = None
282
+
283
+ def put_results(result: str) -> None:
284
+ nonlocal results
285
+ results = result
286
+
287
+ gpts[user_id] = put_results
288
+
289
+ await clients[user_id](
290
+ Mdata(
291
+ data=bash_interaction,
292
+ user_id=user_id,
293
+ )
294
+ )
295
+
296
+ start_time = time.time()
297
+ while time.time() - start_time < 30:
298
+ if results is not None:
299
+ return results
300
+ await asyncio.sleep(0.1)
301
+
302
+ raise fastapi.HTTPException(status_code=500, detail="Timeout error")
303
+
304
+
305
+ app.mount("/static", StaticFiles(directory="static"), name="static")
306
+
307
+
308
+ def run() -> None:
309
+ load_dotenv()
310
+
311
+ uvicorn_thread = threading.Thread(
312
+ target=uvicorn.run,
313
+ args=(app,),
314
+ kwargs={
315
+ "host": "0.0.0.0",
316
+ "port": 8000,
317
+ "log_level": "info",
318
+ "access_log": True,
319
+ },
320
+ )
321
+ uvicorn_thread.start()
322
+ uvicorn_thread.join()
323
+
324
+
325
+ if __name__ == "__main__":
326
+ run()
@@ -0,0 +1,7 @@
1
+ Privacy Policy
2
+ I do not collect, store, or share any personal data.
3
+ The data from your terminal is not stored anywhere and it's not logged or collected in any form.
4
+ There is a relay webserver for connecting your terminal to chatgpt the source code for which is open at https://github.com/rusiaaman/wcgw/tree/main/src/relay that you can run on your own.
5
+ Other than the relay webserver there is no further involvement of my servers or services.
6
+ Feel free to me contact at info@arcfu.com for questions on privacy or anything else.
7
+
wcgw/types_.py ADDED
@@ -0,0 +1,37 @@
1
+ from typing import Literal, Optional, Sequence
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class BashCommand(BaseModel):
6
+ command: str
7
+
8
+
9
+ Specials = Literal[
10
+ "Key-up", "Key-down", "Key-left", "Key-right", "Enter", "Ctrl-c", "Ctrl-d", "Ctrl-z"
11
+ ]
12
+
13
+
14
+ class BashInteraction(BaseModel):
15
+ send_text: Optional[str] = None
16
+ send_specials: Optional[Sequence[Specials]] = None
17
+ send_ascii: Optional[Sequence[int]] = None
18
+
19
+
20
+ class ReadImage(BaseModel):
21
+ file_path: str
22
+ type: Literal["ReadImage"] = "ReadImage"
23
+
24
+
25
+ class Writefile(BaseModel):
26
+ file_path: str
27
+ file_content: str
28
+
29
+
30
+ class FileEditFindReplace(BaseModel):
31
+ file_path: str
32
+ find_lines: str
33
+ replace_with_lines: str
34
+
35
+
36
+ class ResetShell(BaseModel):
37
+ should_reset: Literal[True] = True
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: wcgw
3
- Version: 0.1.2
3
+ Version: 1.0.0
4
4
  Summary: What could go wrong giving full shell access to chatgpt?
5
5
  Project-URL: Homepage, https://github.com/rusiaaman/wcgw
6
6
  Author-email: Aman Rusia <gapypi@arcfu.com>
7
7
  Requires-Python: <3.13,>=3.10
8
8
  Requires-Dist: fastapi>=0.115.0
9
9
  Requires-Dist: mypy>=1.11.2
10
+ Requires-Dist: nltk>=3.9.1
10
11
  Requires-Dist: openai>=1.46.0
11
12
  Requires-Dist: petname>=2.6
12
13
  Requires-Dist: pexpect>=4.9.0
@@ -14,6 +15,7 @@ Requires-Dist: pydantic>=2.9.2
14
15
  Requires-Dist: pyte>=0.8.2
15
16
  Requires-Dist: python-dotenv>=1.0.1
16
17
  Requires-Dist: rich>=13.8.1
18
+ Requires-Dist: semantic-version>=2.10.0
17
19
  Requires-Dist: shell>=1.0.1
18
20
  Requires-Dist: tiktoken==0.7.0
19
21
  Requires-Dist: toml>=0.10.2
@@ -32,7 +34,8 @@ A custom gpt on chatgpt web app to interact with your local shell.
32
34
  ### 🚀 Highlights
33
35
  - ⚡ **Full Shell Access**: No restrictions, complete control.
34
36
  - ⚡ **Create, Execute, Iterate**: Ask the gpt to keep running compiler checks till all errors are fixed, or ask it to keep checking for the status of a long running command till it's done.
35
- - ⚡ **Interactive Command Handling**: [beta] Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
37
+ - ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
38
+ - ⚡ **REPL support**: [beta] Supports python/node and other REPL execution.
36
39
 
37
40
  ### 🪜 Steps:
38
41
  1. Run the [cli client](https://github.com/rusiaaman/wcgw?tab=readme-ov-file#client) in any directory of choice.
@@ -103,7 +106,7 @@ Run the server
103
106
  If you don't have public ip and domain name, you can use `ngrok` or similar services to get a https address to the api.
104
107
 
105
108
  The specify the server url in the `wcgw` command like so
106
- `wcgw --server-url https://your-url/register`
109
+ `wcgw --server-url https://your-url/v1/register`
107
110
 
108
111
  # [Optional] Local shell access with openai API key
109
112
 
@@ -0,0 +1,15 @@
1
+ wcgw/__init__.py,sha256=VMi3gCAN_4_Ft8v5dY74u6bt5X-H1QhFJqTMZRd4fvk,76
2
+ wcgw/types_.py,sha256=qpMRh1y136GkjIONIFowLy56v5p0OcdNqEH2mTmHPqU,756
3
+ wcgw/client/__main__.py,sha256=ngI_vBcLAv7fJgmS4w4U7tuWtalGB8c7W5qebuT6Z6o,30
4
+ wcgw/client/basic.py,sha256=2rY5pKm9dBBRuku0uIBUeLWjNc3iXjTSK9CmfuTxoS8,16196
5
+ wcgw/client/claude.py,sha256=Bp45-UMBIJd-4tzX618nu-SpRbVtkTb1Es6c_gW6xy0,14861
6
+ wcgw/client/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
7
+ wcgw/client/openai_adapters.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ wcgw/client/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
9
+ wcgw/client/tools.py,sha256=UHPbpaQ70sFzE5_TYYyhV2FAE6BcbEgyRrTexaIpT6Y,21108
10
+ wcgw/relay/serve.py,sha256=y2jGbZ0vKKONOD8TkJjSAaH8N6qGLa-rFtj5DxjUFIw,8916
11
+ wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
12
+ wcgw-1.0.0.dist-info/METADATA,sha256=8OScNwT6aecVULmjuk2oEPNIYGrhhMaAdybsdm57kYM,5217
13
+ wcgw-1.0.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
14
+ wcgw-1.0.0.dist-info/entry_points.txt,sha256=WlIB825-Vm9ZtNzgENQsbHj4DRMkbpVR7uSkQyBlaPA,93
15
+ wcgw-1.0.0.dist-info/RECORD,,
@@ -1,3 +1,4 @@
1
1
  [console_scripts]
2
2
  wcgw = wcgw:listen
3
3
  wcgw_local = wcgw:app
4
+ wcgw_relay = wcgw.relay.serve:run
wcgw/__main__.py DELETED
@@ -1,3 +0,0 @@
1
- from wcgw.tools import run
2
-
3
- run()
@@ -1,12 +0,0 @@
1
- wcgw/__init__.py,sha256=okSsOWpTKDjEQzgOin3Kdpx4Mc3MFX1RunjopHQSIWE,62
2
- wcgw/__main__.py,sha256=MjJnFwfYzA1rW47xuSP1EVsi53DTHeEGqESkQwsELFQ,34
3
- wcgw/basic.py,sha256=z1RVJMTDE1-J33nAPSfMZDdJBliSPCFh55SDCvtDLFI,16198
4
- wcgw/claude.py,sha256=Bp45-UMBIJd-4tzX618nu-SpRbVtkTb1Es6c_gW6xy0,14861
5
- wcgw/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
6
- wcgw/openai_adapters.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- wcgw/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
8
- wcgw/tools.py,sha256=PDfSd6hcL60kaY947txztECWSDoGJrX_DBTz-bTfocY,16811
9
- wcgw-0.1.2.dist-info/METADATA,sha256=ramx2gvuo6NimIPSyN-vQPTAAXneklziWeBDNl9kXAA,5076
10
- wcgw-0.1.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
11
- wcgw-0.1.2.dist-info/entry_points.txt,sha256=T-IH7w6Vc650hr8xksC8kJfbJR4uwN8HDudejwDwrNM,59
12
- wcgw-0.1.2.dist-info/RECORD,,
File without changes
File without changes
File without changes
File without changes
File without changes