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 +2 -2
- wcgw/client/__main__.py +3 -0
- wcgw/{basic.py → client/basic.py} +16 -11
- wcgw/{tools.py → client/tools.py} +295 -130
- wcgw/relay/serve.py +326 -0
- wcgw/relay/static/privacy.txt +7 -0
- wcgw/types_.py +37 -0
- {wcgw-0.1.2.dist-info → wcgw-1.0.0.dist-info}/METADATA +6 -3
- wcgw-1.0.0.dist-info/RECORD +15 -0
- {wcgw-0.1.2.dist-info → wcgw-1.0.0.dist-info}/entry_points.txt +1 -0
- wcgw/__main__.py +0 -3
- wcgw-0.1.2.dist-info/RECORD +0 -12
- /wcgw/{claude.py → client/claude.py} +0 -0
- /wcgw/{common.py → client/common.py} +0 -0
- /wcgw/{openai_adapters.py → client/openai_adapters.py} +0 -0
- /wcgw/{openai_utils.py → client/openai_utils.py} +0 -0
- {wcgw-0.1.2.dist-info → wcgw-1.0.0.dist-info}/WHEEL +0 -0
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
|
wcgw/client/__main__.py
ADDED
|
@@ -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
|
|
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
|
-
|
|
158
|
+
BashCommand,
|
|
160
159
|
description="""
|
|
161
|
-
- Execute a bash
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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":
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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.
|
|
167
|
-
console.print(f"Sending
|
|
168
|
-
for char in bash_arg.
|
|
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
|
-
|
|
185
|
-
|
|
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.
|
|
189
|
-
SHELL
|
|
190
|
-
|
|
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([
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
222
|
-
output +=
|
|
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 == "
|
|
383
|
+
if BASH_STATE == "pending":
|
|
273
384
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
346
|
-
|
|
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
|
-
|
|
|
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
|
|
366
|
-
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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",
|
|
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.
|
|
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**:
|
|
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,,
|
wcgw/__main__.py
DELETED
wcgw-0.1.2.dist-info/RECORD
DELETED
|
@@ -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
|