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":
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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([
|
|
193
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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=
|
|
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=
|
|
9
|
-
wcgw-0.
|
|
10
|
-
wcgw-0.
|
|
11
|
-
wcgw-0.
|
|
12
|
-
wcgw-0.
|
|
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
|
|
File without changes
|