wcgw 0.1.1__py3-none-any.whl → 0.2.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/basic.py CHANGED
@@ -159,7 +159,7 @@ def loop(
159
159
  ExecuteBash,
160
160
  description="""
161
161
  - Execute a bash script. This is stateful (beware with subsequent calls).
162
- - Execute commands using `execute_command` attribute.
162
+ - Execute commands using `execute_command` attribute. You can run python/node/other REPL code lines using `execute_command` too.
163
163
  - Do not use interactive commands like nano. Prefer writing simpler commands.
164
164
  - Last line will always be `(exit <int code>)` except if
165
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]`.
@@ -167,6 +167,7 @@ def loop(
167
167
  - Optionally `exit shell has restarted` is the output, in which case environment resets, you can run fresh commands.
168
168
  - The first line might be `(...truncated)` if the output is too long.
169
169
  - Always run `pwd` if you get any file or directory not found error to make sure you're not lost.
170
+ - You can run python/node/other REPL code lines using `execute_command` too. NOTE: `execute_command` doesn't create a new shell, it uses the same shell.
170
171
  """,
171
172
  ),
172
173
  openai.pydantic_function_tool(
wcgw/tools.py CHANGED
@@ -2,6 +2,7 @@ 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
7
8
  import traceback
@@ -77,17 +78,20 @@ class Writefile(BaseModel):
77
78
  file_content: str
78
79
 
79
80
 
81
+ PROMPT = "#@@"
82
+
83
+
80
84
  def start_shell() -> pexpect.spawn:
81
85
  SHELL = pexpect.spawn(
82
86
  "/bin/bash --noprofile --norc",
83
- env={**os.environ, **{"PS1": "#@@"}}, # type: ignore[arg-type]
87
+ env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
84
88
  echo=False,
85
89
  encoding="utf-8",
86
90
  timeout=TIMEOUT,
87
91
  )
88
- SHELL.expect("#@@")
92
+ SHELL.expect(PROMPT)
89
93
  SHELL.sendline("stty -icanon -echo")
90
- SHELL.expect("#@@")
94
+ SHELL.expect(PROMPT)
91
95
  return SHELL
92
96
 
93
97
 
@@ -103,18 +107,25 @@ def _is_int(mystr: str) -> bool:
103
107
 
104
108
 
105
109
  def _get_exit_code() -> int:
110
+ if PROMPT != "#@@":
111
+ return 0
106
112
  # First reset the prompt in case venv was sourced or other reasons.
107
- SHELL.sendline('export PS1="#@@"')
108
- SHELL.expect("#@@")
113
+ SHELL.sendline(f"export PS1={PROMPT}")
114
+ SHELL.expect(PROMPT)
109
115
  # Reset echo also if it was enabled
110
116
  SHELL.sendline("stty -icanon -echo")
111
- SHELL.expect("#@@")
117
+ SHELL.expect(PROMPT)
112
118
  SHELL.sendline("echo $?")
113
119
  before = ""
114
120
  while not _is_int(before): # Consume all previous output
115
- SHELL.expect("#@@")
121
+ try:
122
+ SHELL.expect(PROMPT)
123
+ except pexpect.TIMEOUT:
124
+ print(f"Couldn't get exit code, before: {before}")
125
+ raise
116
126
  assert isinstance(SHELL.before, str)
117
- before = SHELL.before
127
+ # Render because there could be some anscii escape sequences still set like in google colab env
128
+ before = render_terminal_output(SHELL.before).strip()
118
129
 
119
130
  try:
120
131
  return int((before))
@@ -135,17 +146,51 @@ BASH_STATE: BASH_CLF_OUTPUT = "running"
135
146
 
136
147
 
137
148
  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.
138
- 1. Get its output using `send_ascii: [10]`
149
+ 1. Get its output using `send_ascii: [10] or send_ascii: ["Enter"]`
139
150
  2. Use `send_ascii` to give inputs to the running program, don't use `execute_command` OR
140
151
  3. kill the previous program by sending ctrl+c first using `send_ascii`"""
141
152
 
142
153
 
154
+ def update_repl_prompt(command: str) -> bool:
155
+ global PROMPT
156
+ if re.match(r"^wcgw_update_prompt\(\)$", command.strip()):
157
+ SHELL.sendintr()
158
+ index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
159
+ if index == 0:
160
+ return False
161
+ before = SHELL.before or ""
162
+ assert before, "Something went wrong updating repl prompt"
163
+ PROMPT = before.split("\n")[-1].strip()
164
+ # Escape all regex
165
+ PROMPT = re.escape(PROMPT)
166
+ print(f"Trying to update prompt to: {PROMPT.encode()!r}")
167
+ index = 0
168
+ while index == 0:
169
+ # Consume all REPL prompts till now
170
+ index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
171
+ print(f"Prompt updated to: {PROMPT}")
172
+ return True
173
+ return False
174
+
175
+
143
176
  def execute_bash(
144
177
  enc: tiktoken.Encoding, bash_arg: ExecuteBash, max_tokens: Optional[int]
145
178
  ) -> tuple[str, float]:
146
179
  global SHELL, BASH_STATE
147
180
  try:
181
+ is_interrupt = False
148
182
  if bash_arg.execute_command:
183
+ updated_repl_mode = update_repl_prompt(bash_arg.execute_command)
184
+ if updated_repl_mode:
185
+ BASH_STATE = "running"
186
+ response = "Prompt updated, you can execute REPL lines using execute_command now"
187
+ console.print(response)
188
+ return (
189
+ response,
190
+ 0,
191
+ )
192
+
193
+ console.print(f"$ {bash_arg.execute_command}")
149
194
  if BASH_STATE == "waiting_for_input":
150
195
  raise ValueError(WAITING_INPUT_MESSAGE)
151
196
  elif BASH_STATE == "wont_exit":
@@ -159,14 +204,14 @@ def execute_bash(
159
204
  raise ValueError(
160
205
  "Command should not contain newline character in middle. Run only one command at a time."
161
206
  )
162
-
163
- console.print(f"$ {command}")
164
207
  SHELL.sendline(command)
165
208
  elif bash_arg.send_ascii:
166
209
  console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
167
210
  for char in bash_arg.send_ascii:
168
211
  if isinstance(char, int):
169
212
  SHELL.send(chr(char))
213
+ if char == 3:
214
+ is_interrupt = True
170
215
  if char == "Key-up":
171
216
  SHELL.send("\033[A")
172
217
  elif char == "Key-down":
@@ -179,6 +224,7 @@ def execute_bash(
179
224
  SHELL.send("\n")
180
225
  elif char == "Ctrl-c":
181
226
  SHELL.sendintr()
227
+ is_interrupt = True
182
228
  else:
183
229
  raise Exception("Nothing to send")
184
230
  BASH_STATE = "running"
@@ -189,16 +235,11 @@ def execute_bash(
189
235
  raise
190
236
 
191
237
  wait = 5
192
- index = SHELL.expect(["#@@", pexpect.TIMEOUT], timeout=wait)
193
- running = ""
194
- while index == 1:
195
- if wait > TIMEOUT:
196
- raise TimeoutError("Timeout while waiting for shell prompt")
197
-
238
+ index = SHELL.expect([PROMPT, pexpect.TIMEOUT], timeout=wait)
239
+ if index == 1:
198
240
  BASH_STATE = "waiting_for_input"
199
241
  text = SHELL.before or ""
200
- print(text[len(running) :])
201
- running = text
242
+ print(text)
202
243
 
203
244
  text = render_terminal_output(text)
204
245
  tokens = enc.encode(text)
@@ -207,7 +248,21 @@ def execute_bash(
207
248
  text = "...(truncated)\n" + enc.decode(tokens[-(max_tokens - 1) :])
208
249
 
209
250
  last_line = "(pending)"
210
- return text + f"\n{last_line}", 0
251
+ text = text + f"\n{last_line}"
252
+
253
+ if is_interrupt:
254
+ text = (
255
+ text
256
+ + """
257
+ Failure interrupting. Have you entered a new REPL like python, node, ipython, etc.? Or have you exited from a previous REPL program?
258
+ If yes:
259
+ Run execute_command: "wcgw_update_prompt()" to enter the new REPL mode.
260
+ If no:
261
+ Try Ctrl-c or Ctrl-d again.
262
+ """
263
+ )
264
+
265
+ return text, 0
211
266
 
212
267
  assert isinstance(SHELL.before, str)
213
268
  output = render_terminal_output(SHELL.before)
@@ -283,9 +338,9 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
283
338
  def read_image_from_shell(file_path: str) -> ImageData:
284
339
  if not os.path.isabs(file_path):
285
340
  SHELL.sendline("pwd")
286
- SHELL.expect("#@@")
341
+ SHELL.expect(PROMPT)
287
342
  assert isinstance(SHELL.before, str)
288
- current_dir = SHELL.before.strip()
343
+ current_dir = render_terminal_output(SHELL.before).strip()
289
344
  file_path = os.path.join(current_dir, file_path)
290
345
 
291
346
  if not os.path.exists(file_path):
@@ -302,9 +357,9 @@ def read_image_from_shell(file_path: str) -> ImageData:
302
357
  def write_file(writefile: Writefile) -> str:
303
358
  if not os.path.isabs(writefile.file_path):
304
359
  SHELL.sendline("pwd")
305
- SHELL.expect("#@@")
360
+ SHELL.expect(PROMPT)
306
361
  assert isinstance(SHELL.before, str)
307
- current_dir = SHELL.before.strip()
362
+ current_dir = render_terminal_output(SHELL.before).strip()
308
363
  return f"Failure: Use absolute path only. FYI current working directory is '{current_dir}'"
309
364
  os.makedirs(os.path.dirname(writefile.file_path), exist_ok=True)
310
365
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: wcgw
3
- Version: 0.1.1
3
+ Version: 0.2.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>
@@ -71,9 +71,9 @@ NOTE: you can resume a broken connection
71
71
  `wcgw --client-uuid $previous_uuid`
72
72
 
73
73
  # How it works
74
- Your commands are relayed through a server I've hosted at https://wcgw.arcfu.com. The code for that is at `src/relay/serve.py`.
74
+ Your commands are relayed through a server to the terminal client. [You could host the server on your own](https://github.com/rusiaaman/wcgw?tab=readme-ov-file#creating-your-own-custom-gpt-and-the-relay-server). For public convenience I've hosted one at https://wcgw.arcfu.com thanks to the gcloud free tier plan.
75
75
 
76
- Chat gpt sends a request to the relay server using the user id that you share with it. The relay server holds a websocket with the terminal client against the user id and acts as a proxy to pass the request.
76
+ Chatgpt sends a request to the relay server using the user id that you share with it. The relay server holds a websocket with the terminal client against the user id and acts as a proxy to pass the request.
77
77
 
78
78
  It's secure in both the directions. Either a malicious actor or a malicious Chatgpt has to correctly guess your UUID for any security breach.
79
79
 
@@ -1,12 +1,12 @@
1
1
  wcgw/__init__.py,sha256=okSsOWpTKDjEQzgOin3Kdpx4Mc3MFX1RunjopHQSIWE,62
2
2
  wcgw/__main__.py,sha256=MjJnFwfYzA1rW47xuSP1EVsi53DTHeEGqESkQwsELFQ,34
3
- wcgw/basic.py,sha256=z1RVJMTDE1-J33nAPSfMZDdJBliSPCFh55SDCvtDLFI,16198
3
+ wcgw/basic.py,sha256=aTos3c0URl-ufgXfQ1bkg-5oFCR_SxG_VI5qckBtex0,16426
4
4
  wcgw/claude.py,sha256=Bp45-UMBIJd-4tzX618nu-SpRbVtkTb1Es6c_gW6xy0,14861
5
5
  wcgw/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
6
6
  wcgw/openai_adapters.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  wcgw/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
8
- wcgw/tools.py,sha256=0cbDs8WDYa_BOrj6_SOxFXZJ0CbDx7T24gooH2J4jG4,16627
9
- wcgw-0.1.1.dist-info/METADATA,sha256=j63XR1wjlTJIqu-dmwYEWTKW-WrBW01FFR4WHPUYaF4,4892
10
- wcgw-0.1.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
11
- wcgw-0.1.1.dist-info/entry_points.txt,sha256=T-IH7w6Vc650hr8xksC8kJfbJR4uwN8HDudejwDwrNM,59
12
- wcgw-0.1.1.dist-info/RECORD,,
8
+ wcgw/tools.py,sha256=UdSU6lAbOGNdG2wiM5x8YTosBDlphiMEo7MHtMjGvRk,18618
9
+ wcgw-0.2.0.dist-info/METADATA,sha256=X4vyv9Oaq8JtD301hk8jObC3bHggqtyWcxNvUssG-I4,5076
10
+ wcgw-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
11
+ wcgw-0.2.0.dist-info/entry_points.txt,sha256=T-IH7w6Vc650hr8xksC8kJfbJR4uwN8HDudejwDwrNM,59
12
+ wcgw-0.2.0.dist-info/RECORD,,
File without changes