wcgw 2.0.4__py3-none-any.whl → 2.1.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/client/anthropic_client.py +0 -2
- wcgw/client/computer_use.py +0 -1
- wcgw/client/openai_client.py +0 -2
- wcgw/client/tools.py +140 -92
- {wcgw-2.0.4.dist-info → wcgw-2.1.0.dist-info}/METADATA +16 -3
- {wcgw-2.0.4.dist-info → wcgw-2.1.0.dist-info}/RECORD +8 -8
- {wcgw-2.0.4.dist-info → wcgw-2.1.0.dist-info}/WHEEL +0 -0
- {wcgw-2.0.4.dist-info → wcgw-2.1.0.dist-info}/entry_points.txt +0 -0
wcgw/client/anthropic_client.py
CHANGED
wcgw/client/computer_use.py
CHANGED
wcgw/client/openai_client.py
CHANGED
wcgw/client/tools.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import base64
|
|
3
3
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
|
+
import datetime
|
|
4
5
|
from io import BytesIO
|
|
5
6
|
import json
|
|
6
7
|
import mimetypes
|
|
@@ -23,6 +24,7 @@ from typing import (
|
|
|
23
24
|
TypedDict,
|
|
24
25
|
)
|
|
25
26
|
import uuid
|
|
27
|
+
import humanize
|
|
26
28
|
from pydantic import BaseModel, TypeAdapter
|
|
27
29
|
import typer
|
|
28
30
|
from .computer_use import run_computer_tool
|
|
@@ -107,19 +109,19 @@ PROMPT = PROMPT_CONST
|
|
|
107
109
|
|
|
108
110
|
def start_shell() -> pexpect.spawn: # type: ignore
|
|
109
111
|
try:
|
|
110
|
-
|
|
112
|
+
shell = pexpect.spawn(
|
|
111
113
|
"/bin/bash",
|
|
112
114
|
env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
|
|
113
115
|
echo=False,
|
|
114
116
|
encoding="utf-8",
|
|
115
117
|
timeout=TIMEOUT,
|
|
116
118
|
)
|
|
117
|
-
|
|
119
|
+
shell.sendline(f"export PS1={PROMPT}")
|
|
118
120
|
except Exception as e:
|
|
119
121
|
traceback.print_exc()
|
|
120
122
|
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
121
123
|
|
|
122
|
-
|
|
124
|
+
shell = pexpect.spawn(
|
|
123
125
|
"/bin/bash --noprofile --norc",
|
|
124
126
|
env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
|
|
125
127
|
echo=False,
|
|
@@ -127,13 +129,10 @@ def start_shell() -> pexpect.spawn: # type: ignore
|
|
|
127
129
|
timeout=TIMEOUT,
|
|
128
130
|
)
|
|
129
131
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
SHELL = start_shell()
|
|
132
|
+
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
133
|
+
shell.sendline("stty -icanon -echo")
|
|
134
|
+
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
135
|
+
return shell
|
|
137
136
|
|
|
138
137
|
|
|
139
138
|
def _is_int(mystr: str) -> bool:
|
|
@@ -144,26 +143,26 @@ def _is_int(mystr: str) -> bool:
|
|
|
144
143
|
return False
|
|
145
144
|
|
|
146
145
|
|
|
147
|
-
def _get_exit_code() -> int:
|
|
146
|
+
def _get_exit_code(shell: pexpect.spawn) -> int: # type: ignore
|
|
148
147
|
if PROMPT != PROMPT_CONST:
|
|
149
148
|
return 0
|
|
150
149
|
# First reset the prompt in case venv was sourced or other reasons.
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
shell.sendline(f"export PS1={PROMPT}")
|
|
151
|
+
shell.expect(PROMPT, timeout=0.2)
|
|
153
152
|
# Reset echo also if it was enabled
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
shell.sendline("stty -icanon -echo")
|
|
154
|
+
shell.expect(PROMPT, timeout=0.2)
|
|
155
|
+
shell.sendline("echo $?")
|
|
157
156
|
before = ""
|
|
158
157
|
while not _is_int(before): # Consume all previous output
|
|
159
158
|
try:
|
|
160
|
-
|
|
159
|
+
shell.expect(PROMPT, timeout=0.2)
|
|
161
160
|
except pexpect.TIMEOUT:
|
|
162
161
|
print(f"Couldn't get exit code, before: {before}")
|
|
163
162
|
raise
|
|
164
|
-
assert isinstance(
|
|
163
|
+
assert isinstance(shell.before, str)
|
|
165
164
|
# Render because there could be some anscii escape sequences still set like in google colab env
|
|
166
|
-
before = render_terminal_output(
|
|
165
|
+
before = render_terminal_output(shell.before).strip()
|
|
167
166
|
|
|
168
167
|
try:
|
|
169
168
|
return int((before))
|
|
@@ -172,9 +171,71 @@ def _get_exit_code() -> int:
|
|
|
172
171
|
|
|
173
172
|
|
|
174
173
|
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class BashState:
|
|
177
|
+
def __init__(self) -> None:
|
|
178
|
+
self._init()
|
|
179
|
+
|
|
180
|
+
def _init(self) -> None:
|
|
181
|
+
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
182
|
+
self._is_in_docker: Optional[str] = ""
|
|
183
|
+
self._cwd: str = os.getcwd()
|
|
184
|
+
self._shell = start_shell()
|
|
185
|
+
|
|
186
|
+
# Get exit info to ensure shell is ready
|
|
187
|
+
_get_exit_code(self._shell)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def shell(self) -> pexpect.spawn: # type: ignore
|
|
191
|
+
return self._shell
|
|
192
|
+
|
|
193
|
+
def set_pending(self) -> None:
|
|
194
|
+
if not isinstance(self._state, datetime.datetime):
|
|
195
|
+
self._state = datetime.datetime.now()
|
|
196
|
+
|
|
197
|
+
def set_repl(self) -> None:
|
|
198
|
+
self._state = "repl"
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def state(self) -> BASH_CLF_OUTPUT:
|
|
202
|
+
if self._state == "repl":
|
|
203
|
+
return "repl"
|
|
204
|
+
return "pending"
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def is_in_docker(self) -> Optional[str]:
|
|
208
|
+
return self._is_in_docker
|
|
209
|
+
|
|
210
|
+
def set_in_docker(self, docker_image_id: str) -> None:
|
|
211
|
+
self._is_in_docker = docker_image_id
|
|
212
|
+
|
|
213
|
+
@property
|
|
214
|
+
def cwd(self) -> str:
|
|
215
|
+
return self._cwd
|
|
216
|
+
|
|
217
|
+
def update_cwd(self) -> str:
|
|
218
|
+
BASH_STATE.shell.sendline("pwd")
|
|
219
|
+
BASH_STATE.shell.expect(PROMPT, timeout=0.2)
|
|
220
|
+
assert isinstance(BASH_STATE.shell.before, str)
|
|
221
|
+
current_dir = render_terminal_output(BASH_STATE.shell.before).strip()
|
|
222
|
+
self._cwd = current_dir
|
|
223
|
+
return current_dir
|
|
224
|
+
|
|
225
|
+
def reset(self) -> None:
|
|
226
|
+
self.shell.close(True)
|
|
227
|
+
self._init()
|
|
228
|
+
|
|
229
|
+
def get_pending_for(self) -> str:
|
|
230
|
+
if isinstance(self._state, datetime.datetime):
|
|
231
|
+
timedelta = datetime.datetime.now() - self._state
|
|
232
|
+
return humanize.naturaldelta(
|
|
233
|
+
timedelta + datetime.timedelta(seconds=TIMEOUT)
|
|
234
|
+
)
|
|
235
|
+
return "Not pending"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
BASH_STATE = BashState()
|
|
178
239
|
|
|
179
240
|
|
|
180
241
|
def initial_info() -> str:
|
|
@@ -183,18 +244,13 @@ def initial_info() -> str:
|
|
|
183
244
|
return f"""
|
|
184
245
|
System: {uname_sysname}
|
|
185
246
|
Machine: {uname_machine}
|
|
186
|
-
Current working directory: {
|
|
247
|
+
Current working directory: {BASH_STATE.cwd}
|
|
187
248
|
wcgw version: {importlib.metadata.version("wcgw")}
|
|
188
249
|
"""
|
|
189
250
|
|
|
190
251
|
|
|
191
252
|
def reset_shell() -> str:
|
|
192
|
-
|
|
193
|
-
SHELL.close(True)
|
|
194
|
-
SHELL = start_shell()
|
|
195
|
-
BASH_STATE = "repl"
|
|
196
|
-
IS_IN_DOCKER = ""
|
|
197
|
-
CWD = os.getcwd()
|
|
253
|
+
BASH_STATE.reset()
|
|
198
254
|
return "Reset successful" + get_status()
|
|
199
255
|
|
|
200
256
|
|
|
@@ -209,11 +265,11 @@ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run mul
|
|
|
209
265
|
def update_repl_prompt(command: str) -> bool:
|
|
210
266
|
global PROMPT
|
|
211
267
|
if re.match(r"^wcgw_update_prompt\(\)$", command.strip()):
|
|
212
|
-
|
|
213
|
-
index =
|
|
268
|
+
BASH_STATE.shell.sendintr()
|
|
269
|
+
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
|
|
214
270
|
if index == 0:
|
|
215
271
|
return False
|
|
216
|
-
before =
|
|
272
|
+
before = BASH_STATE.shell.before or ""
|
|
217
273
|
assert before, "Something went wrong updating repl prompt"
|
|
218
274
|
PROMPT = before.split("\n")[-1].strip()
|
|
219
275
|
# Escape all regex
|
|
@@ -222,33 +278,24 @@ def update_repl_prompt(command: str) -> bool:
|
|
|
222
278
|
index = 0
|
|
223
279
|
while index == 0:
|
|
224
280
|
# Consume all REPL prompts till now
|
|
225
|
-
index =
|
|
281
|
+
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
|
|
226
282
|
print(f"Prompt updated to: {PROMPT}")
|
|
227
283
|
return True
|
|
228
284
|
return False
|
|
229
285
|
|
|
230
286
|
|
|
231
|
-
def get_cwd() -> str:
|
|
232
|
-
SHELL.sendline("pwd")
|
|
233
|
-
SHELL.expect(PROMPT, timeout=0.2)
|
|
234
|
-
assert isinstance(SHELL.before, str)
|
|
235
|
-
current_dir = render_terminal_output(SHELL.before).strip()
|
|
236
|
-
return current_dir
|
|
237
|
-
|
|
238
|
-
|
|
239
287
|
def get_status() -> str:
|
|
240
|
-
global CWD
|
|
241
288
|
exit_code: Optional[int] = None
|
|
242
289
|
|
|
243
290
|
status = "\n\n---\n\n"
|
|
244
|
-
if BASH_STATE == "pending":
|
|
291
|
+
if BASH_STATE.state == "pending":
|
|
245
292
|
status += "status = still running\n"
|
|
246
|
-
status += "
|
|
293
|
+
status += "running for = " + BASH_STATE.get_pending_for() + "\n"
|
|
294
|
+
status += "cwd = " + BASH_STATE.cwd + "\n"
|
|
247
295
|
else:
|
|
248
|
-
exit_code = _get_exit_code()
|
|
296
|
+
exit_code = _get_exit_code(BASH_STATE.shell)
|
|
249
297
|
status += f"status = exited with code {exit_code}\n"
|
|
250
|
-
|
|
251
|
-
status += "cwd = " + CWD + "\n"
|
|
298
|
+
status += "cwd = " + BASH_STATE.update_cwd() + "\n"
|
|
252
299
|
|
|
253
300
|
return status.rstrip()
|
|
254
301
|
|
|
@@ -259,13 +306,12 @@ def execute_bash(
|
|
|
259
306
|
max_tokens: Optional[int],
|
|
260
307
|
timeout_s: Optional[float],
|
|
261
308
|
) -> tuple[str, float]:
|
|
262
|
-
global SHELL, BASH_STATE, CWD
|
|
263
309
|
try:
|
|
264
310
|
is_interrupt = False
|
|
265
311
|
if isinstance(bash_arg, BashCommand):
|
|
266
312
|
updated_repl_mode = update_repl_prompt(bash_arg.command)
|
|
267
313
|
if updated_repl_mode:
|
|
268
|
-
BASH_STATE
|
|
314
|
+
BASH_STATE.set_repl()
|
|
269
315
|
response = (
|
|
270
316
|
"Prompt updated, you can execute REPL lines using BashCommand now"
|
|
271
317
|
)
|
|
@@ -276,7 +322,7 @@ def execute_bash(
|
|
|
276
322
|
)
|
|
277
323
|
|
|
278
324
|
console.print(f"$ {bash_arg.command}")
|
|
279
|
-
if BASH_STATE == "pending":
|
|
325
|
+
if BASH_STATE.state == "pending":
|
|
280
326
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
281
327
|
command = bash_arg.command.strip()
|
|
282
328
|
|
|
@@ -285,7 +331,7 @@ def execute_bash(
|
|
|
285
331
|
"Command should not contain newline character in middle. Run only one command at a time."
|
|
286
332
|
)
|
|
287
333
|
|
|
288
|
-
|
|
334
|
+
BASH_STATE.shell.sendline(command)
|
|
289
335
|
|
|
290
336
|
else:
|
|
291
337
|
if (
|
|
@@ -306,29 +352,29 @@ def execute_bash(
|
|
|
306
352
|
console.print(f"Sending special sequence: {bash_arg.send_specials}")
|
|
307
353
|
for char in bash_arg.send_specials:
|
|
308
354
|
if char == "Key-up":
|
|
309
|
-
|
|
355
|
+
BASH_STATE.shell.send("\033[A")
|
|
310
356
|
elif char == "Key-down":
|
|
311
|
-
|
|
357
|
+
BASH_STATE.shell.send("\033[B")
|
|
312
358
|
elif char == "Key-left":
|
|
313
|
-
|
|
359
|
+
BASH_STATE.shell.send("\033[D")
|
|
314
360
|
elif char == "Key-right":
|
|
315
|
-
|
|
361
|
+
BASH_STATE.shell.send("\033[C")
|
|
316
362
|
elif char == "Enter":
|
|
317
|
-
|
|
363
|
+
BASH_STATE.shell.send("\n")
|
|
318
364
|
elif char == "Ctrl-c":
|
|
319
|
-
|
|
365
|
+
BASH_STATE.shell.sendintr()
|
|
320
366
|
is_interrupt = True
|
|
321
367
|
elif char == "Ctrl-d":
|
|
322
|
-
|
|
368
|
+
BASH_STATE.shell.sendintr()
|
|
323
369
|
is_interrupt = True
|
|
324
370
|
elif char == "Ctrl-z":
|
|
325
|
-
|
|
371
|
+
BASH_STATE.shell.send("\x1a")
|
|
326
372
|
else:
|
|
327
373
|
raise Exception(f"Unknown special character: {char}")
|
|
328
374
|
elif bash_arg.send_ascii:
|
|
329
375
|
console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
|
|
330
376
|
for ascii_char in bash_arg.send_ascii:
|
|
331
|
-
|
|
377
|
+
BASH_STATE.shell.send(chr(ascii_char))
|
|
332
378
|
if ascii_char == 3:
|
|
333
379
|
is_interrupt = True
|
|
334
380
|
else:
|
|
@@ -340,7 +386,7 @@ def execute_bash(
|
|
|
340
386
|
|
|
341
387
|
updated_repl_mode = update_repl_prompt(bash_arg.send_text)
|
|
342
388
|
if updated_repl_mode:
|
|
343
|
-
BASH_STATE
|
|
389
|
+
BASH_STATE.set_repl()
|
|
344
390
|
response = "Prompt updated, you can execute REPL lines using BashCommand now"
|
|
345
391
|
console.print(response)
|
|
346
392
|
return (
|
|
@@ -348,20 +394,18 @@ def execute_bash(
|
|
|
348
394
|
0,
|
|
349
395
|
)
|
|
350
396
|
console.print(f"Interact text: {bash_arg.send_text}")
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
BASH_STATE = "repl"
|
|
397
|
+
BASH_STATE.shell.sendline(bash_arg.send_text)
|
|
354
398
|
|
|
355
399
|
except KeyboardInterrupt:
|
|
356
|
-
|
|
357
|
-
|
|
400
|
+
BASH_STATE.shell.sendintr()
|
|
401
|
+
BASH_STATE.shell.expect(PROMPT)
|
|
358
402
|
return "---\n\nFailure: user interrupted the execution", 0.0
|
|
359
403
|
|
|
360
404
|
wait = timeout_s or TIMEOUT
|
|
361
|
-
index =
|
|
405
|
+
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=wait)
|
|
362
406
|
if index == 1:
|
|
363
|
-
BASH_STATE
|
|
364
|
-
text =
|
|
407
|
+
BASH_STATE.set_pending()
|
|
408
|
+
text = BASH_STATE.shell.before or ""
|
|
365
409
|
|
|
366
410
|
text = render_terminal_output(text[-100_000:])
|
|
367
411
|
tokens = enc.encode(text)
|
|
@@ -386,11 +430,13 @@ Otherwise, you may want to try Ctrl-c again or program specific exit interactive
|
|
|
386
430
|
|
|
387
431
|
return text, 0
|
|
388
432
|
|
|
433
|
+
BASH_STATE.set_repl()
|
|
434
|
+
|
|
389
435
|
if is_interrupt:
|
|
390
436
|
return "Interrupt successful", 0.0
|
|
391
437
|
|
|
392
|
-
assert isinstance(
|
|
393
|
-
output = render_terminal_output(
|
|
438
|
+
assert isinstance(BASH_STATE.shell.before, str)
|
|
439
|
+
output = render_terminal_output(BASH_STATE.shell.before)
|
|
394
440
|
|
|
395
441
|
tokens = enc.encode(output)
|
|
396
442
|
if max_tokens and len(tokens) >= max_tokens:
|
|
@@ -404,8 +450,7 @@ Otherwise, you may want to try Ctrl-c again or program specific exit interactive
|
|
|
404
450
|
traceback.print_exc()
|
|
405
451
|
console.print("Malformed output, restarting shell", style="red")
|
|
406
452
|
# Malformed output, restart shell
|
|
407
|
-
|
|
408
|
-
SHELL = start_shell()
|
|
453
|
+
BASH_STATE.reset()
|
|
409
454
|
output = "(exit shell has restarted)"
|
|
410
455
|
return output, 0
|
|
411
456
|
|
|
@@ -449,8 +494,7 @@ T = TypeVar("T")
|
|
|
449
494
|
|
|
450
495
|
def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
451
496
|
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
|
|
452
|
-
|
|
453
|
-
if BASH_STATE == "pending":
|
|
497
|
+
if BASH_STATE.state == "pending":
|
|
454
498
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
455
499
|
|
|
456
500
|
return func(*args, **kwargs)
|
|
@@ -460,9 +504,9 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
|
460
504
|
|
|
461
505
|
def read_image_from_shell(file_path: str) -> ImageData:
|
|
462
506
|
if not os.path.isabs(file_path):
|
|
463
|
-
file_path = os.path.join(
|
|
507
|
+
file_path = os.path.join(BASH_STATE.cwd, file_path)
|
|
464
508
|
|
|
465
|
-
if not
|
|
509
|
+
if not BASH_STATE.is_in_docker:
|
|
466
510
|
if not os.path.exists(file_path):
|
|
467
511
|
raise ValueError(f"File {file_path} does not exist")
|
|
468
512
|
|
|
@@ -473,7 +517,9 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
473
517
|
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
474
518
|
else:
|
|
475
519
|
with TemporaryDirectory() as tmpdir:
|
|
476
|
-
rcode = os.system(
|
|
520
|
+
rcode = os.system(
|
|
521
|
+
f"docker cp {BASH_STATE.is_in_docker}:{file_path} {tmpdir}"
|
|
522
|
+
)
|
|
477
523
|
if rcode != 0:
|
|
478
524
|
raise Exception(f"Error: Read failed with code {rcode}")
|
|
479
525
|
path_ = os.path.join(tmpdir, os.path.basename(file_path))
|
|
@@ -486,11 +532,11 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
486
532
|
|
|
487
533
|
def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
488
534
|
if not os.path.isabs(writefile.file_path):
|
|
489
|
-
return f"Failure: file_path should be absolute path, current working directory is {
|
|
535
|
+
return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
490
536
|
else:
|
|
491
537
|
path_ = writefile.file_path
|
|
492
538
|
|
|
493
|
-
if not
|
|
539
|
+
if not BASH_STATE.is_in_docker:
|
|
494
540
|
if error_on_exist and os.path.exists(path_):
|
|
495
541
|
file_data = Path(path_).read_text()
|
|
496
542
|
if file_data:
|
|
@@ -508,7 +554,7 @@ def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
|
508
554
|
if error_on_exist:
|
|
509
555
|
# Check if it exists using os.system
|
|
510
556
|
cmd = f"test -f {path_}"
|
|
511
|
-
status = os.system(f'docker exec {
|
|
557
|
+
status = os.system(f'docker exec {BASH_STATE.is_in_docker} bash -c "{cmd}"')
|
|
512
558
|
if status == 0:
|
|
513
559
|
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
514
560
|
|
|
@@ -518,11 +564,13 @@ def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
|
518
564
|
f.write(writefile.file_content)
|
|
519
565
|
os.chmod(tmppath, 0o777)
|
|
520
566
|
parent_dir = os.path.dirname(path_)
|
|
521
|
-
rcode = os.system(
|
|
567
|
+
rcode = os.system(
|
|
568
|
+
f"docker exec {BASH_STATE.is_in_docker} mkdir -p {parent_dir}"
|
|
569
|
+
)
|
|
522
570
|
if rcode != 0:
|
|
523
571
|
return f"Error: Write failed with code while creating dirs {rcode}"
|
|
524
572
|
|
|
525
|
-
rcode = os.system(f"docker cp {tmppath} {
|
|
573
|
+
rcode = os.system(f"docker cp {tmppath} {BASH_STATE.is_in_docker}:{path_}")
|
|
526
574
|
if rcode != 0:
|
|
527
575
|
return f"Error: Write failed with code {rcode}"
|
|
528
576
|
|
|
@@ -599,12 +647,12 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
599
647
|
|
|
600
648
|
if not os.path.isabs(fedit.file_path):
|
|
601
649
|
raise Exception(
|
|
602
|
-
f"Failure: file_path should be absolute path, current working directory is {
|
|
650
|
+
f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
603
651
|
)
|
|
604
652
|
else:
|
|
605
653
|
path_ = fedit.file_path
|
|
606
654
|
|
|
607
|
-
if not
|
|
655
|
+
if not BASH_STATE.is_in_docker:
|
|
608
656
|
if not os.path.exists(path_):
|
|
609
657
|
raise Exception(f"Error: file {path_} does not exist")
|
|
610
658
|
|
|
@@ -613,7 +661,7 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
613
661
|
else:
|
|
614
662
|
# Copy from docker
|
|
615
663
|
with TemporaryDirectory() as tmpdir:
|
|
616
|
-
rcode = os.system(f"docker cp {
|
|
664
|
+
rcode = os.system(f"docker cp {BASH_STATE.is_in_docker}:{path_} {tmpdir}")
|
|
617
665
|
if rcode != 0:
|
|
618
666
|
raise Exception(f"Error: Read failed with code {rcode}")
|
|
619
667
|
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
@@ -666,7 +714,7 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
666
714
|
"Error: no valid search-replace blocks found, please check your syntax for FileEdit"
|
|
667
715
|
)
|
|
668
716
|
|
|
669
|
-
if not
|
|
717
|
+
if not BASH_STATE.is_in_docker:
|
|
670
718
|
with open(path_, "w") as f:
|
|
671
719
|
f.write(apply_diff_to)
|
|
672
720
|
else:
|
|
@@ -676,7 +724,7 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
676
724
|
f.write(apply_diff_to)
|
|
677
725
|
os.chmod(path_tmp, 0o777)
|
|
678
726
|
# Copy to docker using docker cp
|
|
679
|
-
rcode = os.system(f"docker cp {path_tmp} {
|
|
727
|
+
rcode = os.system(f"docker cp {path_tmp} {BASH_STATE.is_in_docker}:{path_}")
|
|
680
728
|
if rcode != 0:
|
|
681
729
|
raise Exception(f"Error: Write failed with code {rcode}")
|
|
682
730
|
|
|
@@ -870,7 +918,7 @@ def get_tool_output(
|
|
|
870
918
|
if imgBs64:
|
|
871
919
|
console.print("Captured screenshot")
|
|
872
920
|
outputs.append(ImageData(media_type="image/png", data=imgBs64))
|
|
873
|
-
if not
|
|
921
|
+
if not BASH_STATE.is_in_docker and isinstance(arg, GetScreenInfo):
|
|
874
922
|
try:
|
|
875
923
|
# At this point we should go into the docker env
|
|
876
924
|
res, _ = execute_bash(
|
|
@@ -896,7 +944,7 @@ def get_tool_output(
|
|
|
896
944
|
raise Exception(
|
|
897
945
|
f"Some error happened while going inside docker. I've reset the shell. Please start again. Error {e}"
|
|
898
946
|
)
|
|
899
|
-
|
|
947
|
+
BASH_STATE.set_in_docker(arg.docker_image_id)
|
|
900
948
|
return outputs, outputs_cost[1]
|
|
901
949
|
else:
|
|
902
950
|
raise ValueError(f"Unknown tool: {arg}")
|
|
@@ -1001,9 +1049,9 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
|
|
|
1001
1049
|
console.print(f"Reading file: {readfile.file_path}")
|
|
1002
1050
|
|
|
1003
1051
|
if not os.path.isabs(readfile.file_path):
|
|
1004
|
-
return f"Failure: file_path should be absolute path, current working directory is {
|
|
1052
|
+
return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
1005
1053
|
|
|
1006
|
-
if not
|
|
1054
|
+
if not BASH_STATE.is_in_docker:
|
|
1007
1055
|
path = Path(readfile.file_path)
|
|
1008
1056
|
if not path.exists():
|
|
1009
1057
|
return f"Error: file {readfile.file_path} does not exist"
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Shell and coding agent on claude and chatgpt
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
7
7
|
Requires-Python: <3.13,>=3.11
|
|
8
8
|
Requires-Dist: anthropic>=0.39.0
|
|
9
9
|
Requires-Dist: fastapi>=0.115.0
|
|
10
|
+
Requires-Dist: humanize>=4.11.0
|
|
10
11
|
Requires-Dist: mcp
|
|
11
12
|
Requires-Dist: mypy>=1.11.2
|
|
12
13
|
Requires-Dist: nltk>=3.9.1
|
|
@@ -52,6 +53,7 @@ Description-Content-Type: text/markdown
|
|
|
52
53
|
- ⚡ **REPL support**: [beta] Supports python/node and other REPL execution.
|
|
53
54
|
|
|
54
55
|
## Top use cases examples
|
|
56
|
+
|
|
55
57
|
- Solve problem X using python, create and run test cases and fix any issues. Do it in a temporary directory
|
|
56
58
|
- Find instances of code with X behavior in my repository
|
|
57
59
|
- Git clone https://github.com/my/repo in my home directory, then understand the project, set up the environment and build
|
|
@@ -92,6 +94,13 @@ Then update `claude_desktop_config.json` (~/Library/Application Support/Claude/c
|
|
|
92
94
|
|
|
93
95
|
Then restart claude app.
|
|
94
96
|
|
|
97
|
+
_If there's an error in setting up_
|
|
98
|
+
|
|
99
|
+
- Make sure `uv` in the system PATH by running `uv --version` and also ensure `uv tool run wcgw --version` works globally.
|
|
100
|
+
Otherwise, re-install uv and follow instructions to add it into your .zshrc or .bashrc
|
|
101
|
+
- If there's still an issue, check that `uv tool run --from wcgw@latest --python 3.12 wcgw_mcp` runs in your terminal. It should have no output and shouldn't exit.
|
|
102
|
+
- Debug the mcp server using `npx @modelcontextprotocol/inspector@0.1.7 uv tool run --from wcgw@latest --python 3.12 wcgw_mcp`
|
|
103
|
+
|
|
95
104
|
### [Optional] Computer use support using desktop on docker
|
|
96
105
|
|
|
97
106
|
Computer use is disabled by default. Add `--computer-use` to enable it. This will add necessary tools to Claude including ScreenShot, Mouse and Keyboard control.
|
|
@@ -128,6 +137,12 @@ Then ask claude desktop app to control the docker os. It'll connect to the docke
|
|
|
128
137
|
|
|
129
138
|
Connect to `http://localhost:6080/vnc.html` for desktop view (VNC) of the system running in the docker.
|
|
130
139
|
|
|
140
|
+
The following requirements should be installed and working in the linux docker image:
|
|
141
|
+
|
|
142
|
+
1. Needs `xdotool` to execute commands on the desktop.
|
|
143
|
+
2. Needs `scrot` to take screenshots.
|
|
144
|
+
3. Needs `convert` from imagemagick to convert images.
|
|
145
|
+
|
|
131
146
|
## Usage
|
|
132
147
|
|
|
133
148
|
Wait for a few seconds. You should be able to see this icon if everything goes right.
|
|
@@ -141,7 +156,6 @@ Then ask claude to execute shell commands, read files, edit files, run your code
|
|
|
141
156
|
|
|
142
157
|
If you've run the docker for LLM to access, you can ask it to control the "docker os". If you don't provide the docker container id to it, it'll try to search for available docker using `docker ps` command.
|
|
143
158
|
|
|
144
|
-
|
|
145
159
|
## Chatgpt Setup
|
|
146
160
|
|
|
147
161
|
Read here: https://github.com/rusiaaman/wcgw/blob/main/openai.md
|
|
@@ -156,7 +170,6 @@ Read here: https://github.com/rusiaaman/wcgw/blob/main/openai.md
|
|
|
156
170
|
|
|
157
171
|

|
|
158
172
|
|
|
159
|
-
|
|
160
173
|
## [Optional] Local shell access with openai API key or anthropic API key
|
|
161
174
|
|
|
162
175
|
### Openai
|
|
@@ -2,21 +2,21 @@ wcgw/__init__.py,sha256=9K2QW7QuSLhMTVbKbBYd9UUp-ZyrfBrxcjuD_xk458k,118
|
|
|
2
2
|
wcgw/types_.py,sha256=rDz4olJS2zvYC13jzeOppA2tci-tVDyWAqeA5BesAaU,1773
|
|
3
3
|
wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
wcgw/client/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
5
|
-
wcgw/client/anthropic_client.py,sha256=
|
|
5
|
+
wcgw/client/anthropic_client.py,sha256=fEjBk7RQ_FV7yDyCeWvPGM-0MsYJw538jREZ3F3Txf0,20265
|
|
6
6
|
wcgw/client/cli.py,sha256=-z0kpDAW3mzfQrQeZfaVJhBCAQY3HXnt9GdgQ8s-u0Y,1003
|
|
7
7
|
wcgw/client/common.py,sha256=grH-yV_4tnTQZ29xExn4YicGLxEq98z-HkEZwH0ReSg,1410
|
|
8
|
-
wcgw/client/computer_use.py,sha256=
|
|
8
|
+
wcgw/client/computer_use.py,sha256=35NKAlMrxwD0TBlMMRnbCwz4g8TBRGOlcy-cmS-yJ_A,15247
|
|
9
9
|
wcgw/client/diff-instructions.txt,sha256=s5AJKG23JsjwRYhFZFQVvwDpF67vElawrmdXwvukR1A,1683
|
|
10
|
-
wcgw/client/openai_client.py,sha256=
|
|
10
|
+
wcgw/client/openai_client.py,sha256=qhntpGZANTyI2-vAWaI1pyiJsiOwEjhXc4lCvPpebiM,17752
|
|
11
11
|
wcgw/client/openai_utils.py,sha256=YNwCsA-Wqq7jWrxP0rfQmBTb1dI0s7dWXzQqyTzOZT4,2629
|
|
12
12
|
wcgw/client/sys_utils.py,sha256=GajPntKhaTUMn6EOmopENWZNR2G_BJyuVbuot0x6veI,1376
|
|
13
|
-
wcgw/client/tools.py,sha256=
|
|
13
|
+
wcgw/client/tools.py,sha256=QO_IkOugFrdz1pnX4VBvFvxAjpIDL0wmYmGjU_CkjRc,35504
|
|
14
14
|
wcgw/client/mcp_server/Readme.md,sha256=I8N4dHkTUVGNQ63BQkBMBhCCBTgqGOSF_pUR6iOEiUk,2495
|
|
15
15
|
wcgw/client/mcp_server/__init__.py,sha256=hyPPwO9cabAJsOMWhKyat9yl7OlSmIobaoAZKHu3DMc,381
|
|
16
16
|
wcgw/client/mcp_server/server.py,sha256=XWZCMlL5HOpvRQjY1qoZnmFAe9x_rpRb_udZz9k8ks4,10815
|
|
17
17
|
wcgw/relay/serve.py,sha256=RUcUeyL4Xt0EEo12Ul6VQjb4tRle4uIdsa85v7XXxEw,8771
|
|
18
18
|
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
19
|
-
wcgw-2.0.
|
|
20
|
-
wcgw-2.0.
|
|
21
|
-
wcgw-2.0.
|
|
22
|
-
wcgw-2.0.
|
|
19
|
+
wcgw-2.1.0.dist-info/METADATA,sha256=43N0-Y_Ab6UhhwcLqM5UWaW1gmjo-BuBiZYLn-kDhC4,7421
|
|
20
|
+
wcgw-2.1.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
21
|
+
wcgw-2.1.0.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
|
|
22
|
+
wcgw-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|