wcgw 2.0.4__py3-none-any.whl → 2.1.1__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 +150 -94
- {wcgw-2.0.4.dist-info → wcgw-2.1.1.dist-info}/METADATA +16 -3
- {wcgw-2.0.4.dist-info → wcgw-2.1.1.dist-info}/RECORD +8 -8
- {wcgw-2.0.4.dist-info → wcgw-2.1.1.dist-info}/WHEEL +0 -0
- {wcgw-2.0.4.dist-info → wcgw-2.1.1.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,11 +1,13 @@
|
|
|
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
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
import re
|
|
10
|
+
import shlex
|
|
9
11
|
import sys
|
|
10
12
|
import threading
|
|
11
13
|
import importlib.metadata
|
|
@@ -23,6 +25,7 @@ from typing import (
|
|
|
23
25
|
TypedDict,
|
|
24
26
|
)
|
|
25
27
|
import uuid
|
|
28
|
+
import humanize
|
|
26
29
|
from pydantic import BaseModel, TypeAdapter
|
|
27
30
|
import typer
|
|
28
31
|
from .computer_use import run_computer_tool
|
|
@@ -107,19 +110,19 @@ PROMPT = PROMPT_CONST
|
|
|
107
110
|
|
|
108
111
|
def start_shell() -> pexpect.spawn: # type: ignore
|
|
109
112
|
try:
|
|
110
|
-
|
|
113
|
+
shell = pexpect.spawn(
|
|
111
114
|
"/bin/bash",
|
|
112
115
|
env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
|
|
113
116
|
echo=False,
|
|
114
117
|
encoding="utf-8",
|
|
115
118
|
timeout=TIMEOUT,
|
|
116
119
|
)
|
|
117
|
-
|
|
120
|
+
shell.sendline(f"export PS1={PROMPT}")
|
|
118
121
|
except Exception as e:
|
|
119
122
|
traceback.print_exc()
|
|
120
123
|
console.log(f"Error starting shell: {e}. Retrying without rc ...")
|
|
121
124
|
|
|
122
|
-
|
|
125
|
+
shell = pexpect.spawn(
|
|
123
126
|
"/bin/bash --noprofile --norc",
|
|
124
127
|
env={**os.environ, **{"PS1": PROMPT}}, # type: ignore[arg-type]
|
|
125
128
|
echo=False,
|
|
@@ -127,13 +130,10 @@ def start_shell() -> pexpect.spawn: # type: ignore
|
|
|
127
130
|
timeout=TIMEOUT,
|
|
128
131
|
)
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
SHELL = start_shell()
|
|
133
|
+
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
134
|
+
shell.sendline("stty -icanon -echo")
|
|
135
|
+
shell.expect(PROMPT, timeout=TIMEOUT)
|
|
136
|
+
return shell
|
|
137
137
|
|
|
138
138
|
|
|
139
139
|
def _is_int(mystr: str) -> bool:
|
|
@@ -144,26 +144,26 @@ def _is_int(mystr: str) -> bool:
|
|
|
144
144
|
return False
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
def _get_exit_code() -> int:
|
|
147
|
+
def _get_exit_code(shell: pexpect.spawn) -> int: # type: ignore
|
|
148
148
|
if PROMPT != PROMPT_CONST:
|
|
149
149
|
return 0
|
|
150
150
|
# First reset the prompt in case venv was sourced or other reasons.
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
shell.sendline(f"export PS1={PROMPT}")
|
|
152
|
+
shell.expect(PROMPT, timeout=0.2)
|
|
153
153
|
# Reset echo also if it was enabled
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
shell.sendline("stty -icanon -echo")
|
|
155
|
+
shell.expect(PROMPT, timeout=0.2)
|
|
156
|
+
shell.sendline("echo $?")
|
|
157
157
|
before = ""
|
|
158
158
|
while not _is_int(before): # Consume all previous output
|
|
159
159
|
try:
|
|
160
|
-
|
|
160
|
+
shell.expect(PROMPT, timeout=0.2)
|
|
161
161
|
except pexpect.TIMEOUT:
|
|
162
162
|
print(f"Couldn't get exit code, before: {before}")
|
|
163
163
|
raise
|
|
164
|
-
assert isinstance(
|
|
164
|
+
assert isinstance(shell.before, str)
|
|
165
165
|
# Render because there could be some anscii escape sequences still set like in google colab env
|
|
166
|
-
before = render_terminal_output(
|
|
166
|
+
before = render_terminal_output(shell.before).strip()
|
|
167
167
|
|
|
168
168
|
try:
|
|
169
169
|
return int((before))
|
|
@@ -172,9 +172,71 @@ def _get_exit_code() -> int:
|
|
|
172
172
|
|
|
173
173
|
|
|
174
174
|
BASH_CLF_OUTPUT = Literal["repl", "pending"]
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class BashState:
|
|
178
|
+
def __init__(self) -> None:
|
|
179
|
+
self._init()
|
|
180
|
+
|
|
181
|
+
def _init(self) -> None:
|
|
182
|
+
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
183
|
+
self._is_in_docker: Optional[str] = ""
|
|
184
|
+
self._cwd: str = os.getcwd()
|
|
185
|
+
self._shell = start_shell()
|
|
186
|
+
|
|
187
|
+
# Get exit info to ensure shell is ready
|
|
188
|
+
_get_exit_code(self._shell)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def shell(self) -> pexpect.spawn: # type: ignore
|
|
192
|
+
return self._shell
|
|
193
|
+
|
|
194
|
+
def set_pending(self) -> None:
|
|
195
|
+
if not isinstance(self._state, datetime.datetime):
|
|
196
|
+
self._state = datetime.datetime.now()
|
|
197
|
+
|
|
198
|
+
def set_repl(self) -> None:
|
|
199
|
+
self._state = "repl"
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def state(self) -> BASH_CLF_OUTPUT:
|
|
203
|
+
if self._state == "repl":
|
|
204
|
+
return "repl"
|
|
205
|
+
return "pending"
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def is_in_docker(self) -> Optional[str]:
|
|
209
|
+
return self._is_in_docker
|
|
210
|
+
|
|
211
|
+
def set_in_docker(self, docker_image_id: str) -> None:
|
|
212
|
+
self._is_in_docker = docker_image_id
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def cwd(self) -> str:
|
|
216
|
+
return self._cwd
|
|
217
|
+
|
|
218
|
+
def update_cwd(self) -> str:
|
|
219
|
+
BASH_STATE.shell.sendline("pwd")
|
|
220
|
+
BASH_STATE.shell.expect(PROMPT, timeout=0.2)
|
|
221
|
+
assert isinstance(BASH_STATE.shell.before, str)
|
|
222
|
+
current_dir = render_terminal_output(BASH_STATE.shell.before).strip()
|
|
223
|
+
self._cwd = current_dir
|
|
224
|
+
return current_dir
|
|
225
|
+
|
|
226
|
+
def reset(self) -> None:
|
|
227
|
+
self.shell.close(True)
|
|
228
|
+
self._init()
|
|
229
|
+
|
|
230
|
+
def get_pending_for(self) -> str:
|
|
231
|
+
if isinstance(self._state, datetime.datetime):
|
|
232
|
+
timedelta = datetime.datetime.now() - self._state
|
|
233
|
+
return humanize.naturaldelta(
|
|
234
|
+
timedelta + datetime.timedelta(seconds=TIMEOUT)
|
|
235
|
+
)
|
|
236
|
+
return "Not pending"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
BASH_STATE = BashState()
|
|
178
240
|
|
|
179
241
|
|
|
180
242
|
def initial_info() -> str:
|
|
@@ -183,18 +245,13 @@ def initial_info() -> str:
|
|
|
183
245
|
return f"""
|
|
184
246
|
System: {uname_sysname}
|
|
185
247
|
Machine: {uname_machine}
|
|
186
|
-
Current working directory: {
|
|
248
|
+
Current working directory: {BASH_STATE.cwd}
|
|
187
249
|
wcgw version: {importlib.metadata.version("wcgw")}
|
|
188
250
|
"""
|
|
189
251
|
|
|
190
252
|
|
|
191
253
|
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()
|
|
254
|
+
BASH_STATE.reset()
|
|
198
255
|
return "Reset successful" + get_status()
|
|
199
256
|
|
|
200
257
|
|
|
@@ -209,11 +266,11 @@ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run mul
|
|
|
209
266
|
def update_repl_prompt(command: str) -> bool:
|
|
210
267
|
global PROMPT
|
|
211
268
|
if re.match(r"^wcgw_update_prompt\(\)$", command.strip()):
|
|
212
|
-
|
|
213
|
-
index =
|
|
269
|
+
BASH_STATE.shell.sendintr()
|
|
270
|
+
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
|
|
214
271
|
if index == 0:
|
|
215
272
|
return False
|
|
216
|
-
before =
|
|
273
|
+
before = BASH_STATE.shell.before or ""
|
|
217
274
|
assert before, "Something went wrong updating repl prompt"
|
|
218
275
|
PROMPT = before.split("\n")[-1].strip()
|
|
219
276
|
# Escape all regex
|
|
@@ -222,33 +279,24 @@ def update_repl_prompt(command: str) -> bool:
|
|
|
222
279
|
index = 0
|
|
223
280
|
while index == 0:
|
|
224
281
|
# Consume all REPL prompts till now
|
|
225
|
-
index =
|
|
282
|
+
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=0.2)
|
|
226
283
|
print(f"Prompt updated to: {PROMPT}")
|
|
227
284
|
return True
|
|
228
285
|
return False
|
|
229
286
|
|
|
230
287
|
|
|
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
288
|
def get_status() -> str:
|
|
240
|
-
global CWD
|
|
241
289
|
exit_code: Optional[int] = None
|
|
242
290
|
|
|
243
291
|
status = "\n\n---\n\n"
|
|
244
|
-
if BASH_STATE == "pending":
|
|
292
|
+
if BASH_STATE.state == "pending":
|
|
245
293
|
status += "status = still running\n"
|
|
246
|
-
status += "
|
|
294
|
+
status += "running for = " + BASH_STATE.get_pending_for() + "\n"
|
|
295
|
+
status += "cwd = " + BASH_STATE.cwd + "\n"
|
|
247
296
|
else:
|
|
248
|
-
exit_code = _get_exit_code()
|
|
297
|
+
exit_code = _get_exit_code(BASH_STATE.shell)
|
|
249
298
|
status += f"status = exited with code {exit_code}\n"
|
|
250
|
-
|
|
251
|
-
status += "cwd = " + CWD + "\n"
|
|
299
|
+
status += "cwd = " + BASH_STATE.update_cwd() + "\n"
|
|
252
300
|
|
|
253
301
|
return status.rstrip()
|
|
254
302
|
|
|
@@ -259,13 +307,12 @@ def execute_bash(
|
|
|
259
307
|
max_tokens: Optional[int],
|
|
260
308
|
timeout_s: Optional[float],
|
|
261
309
|
) -> tuple[str, float]:
|
|
262
|
-
global SHELL, BASH_STATE, CWD
|
|
263
310
|
try:
|
|
264
311
|
is_interrupt = False
|
|
265
312
|
if isinstance(bash_arg, BashCommand):
|
|
266
313
|
updated_repl_mode = update_repl_prompt(bash_arg.command)
|
|
267
314
|
if updated_repl_mode:
|
|
268
|
-
BASH_STATE
|
|
315
|
+
BASH_STATE.set_repl()
|
|
269
316
|
response = (
|
|
270
317
|
"Prompt updated, you can execute REPL lines using BashCommand now"
|
|
271
318
|
)
|
|
@@ -276,7 +323,7 @@ def execute_bash(
|
|
|
276
323
|
)
|
|
277
324
|
|
|
278
325
|
console.print(f"$ {bash_arg.command}")
|
|
279
|
-
if BASH_STATE == "pending":
|
|
326
|
+
if BASH_STATE.state == "pending":
|
|
280
327
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
281
328
|
command = bash_arg.command.strip()
|
|
282
329
|
|
|
@@ -285,7 +332,7 @@ def execute_bash(
|
|
|
285
332
|
"Command should not contain newline character in middle. Run only one command at a time."
|
|
286
333
|
)
|
|
287
334
|
|
|
288
|
-
|
|
335
|
+
BASH_STATE.shell.sendline(command)
|
|
289
336
|
|
|
290
337
|
else:
|
|
291
338
|
if (
|
|
@@ -306,29 +353,29 @@ def execute_bash(
|
|
|
306
353
|
console.print(f"Sending special sequence: {bash_arg.send_specials}")
|
|
307
354
|
for char in bash_arg.send_specials:
|
|
308
355
|
if char == "Key-up":
|
|
309
|
-
|
|
356
|
+
BASH_STATE.shell.send("\033[A")
|
|
310
357
|
elif char == "Key-down":
|
|
311
|
-
|
|
358
|
+
BASH_STATE.shell.send("\033[B")
|
|
312
359
|
elif char == "Key-left":
|
|
313
|
-
|
|
360
|
+
BASH_STATE.shell.send("\033[D")
|
|
314
361
|
elif char == "Key-right":
|
|
315
|
-
|
|
362
|
+
BASH_STATE.shell.send("\033[C")
|
|
316
363
|
elif char == "Enter":
|
|
317
|
-
|
|
364
|
+
BASH_STATE.shell.send("\n")
|
|
318
365
|
elif char == "Ctrl-c":
|
|
319
|
-
|
|
366
|
+
BASH_STATE.shell.sendintr()
|
|
320
367
|
is_interrupt = True
|
|
321
368
|
elif char == "Ctrl-d":
|
|
322
|
-
|
|
369
|
+
BASH_STATE.shell.sendintr()
|
|
323
370
|
is_interrupt = True
|
|
324
371
|
elif char == "Ctrl-z":
|
|
325
|
-
|
|
372
|
+
BASH_STATE.shell.send("\x1a")
|
|
326
373
|
else:
|
|
327
374
|
raise Exception(f"Unknown special character: {char}")
|
|
328
375
|
elif bash_arg.send_ascii:
|
|
329
376
|
console.print(f"Sending ASCII sequence: {bash_arg.send_ascii}")
|
|
330
377
|
for ascii_char in bash_arg.send_ascii:
|
|
331
|
-
|
|
378
|
+
BASH_STATE.shell.send(chr(ascii_char))
|
|
332
379
|
if ascii_char == 3:
|
|
333
380
|
is_interrupt = True
|
|
334
381
|
else:
|
|
@@ -340,7 +387,7 @@ def execute_bash(
|
|
|
340
387
|
|
|
341
388
|
updated_repl_mode = update_repl_prompt(bash_arg.send_text)
|
|
342
389
|
if updated_repl_mode:
|
|
343
|
-
BASH_STATE
|
|
390
|
+
BASH_STATE.set_repl()
|
|
344
391
|
response = "Prompt updated, you can execute REPL lines using BashCommand now"
|
|
345
392
|
console.print(response)
|
|
346
393
|
return (
|
|
@@ -348,20 +395,18 @@ def execute_bash(
|
|
|
348
395
|
0,
|
|
349
396
|
)
|
|
350
397
|
console.print(f"Interact text: {bash_arg.send_text}")
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
BASH_STATE = "repl"
|
|
398
|
+
BASH_STATE.shell.sendline(bash_arg.send_text)
|
|
354
399
|
|
|
355
400
|
except KeyboardInterrupt:
|
|
356
|
-
|
|
357
|
-
|
|
401
|
+
BASH_STATE.shell.sendintr()
|
|
402
|
+
BASH_STATE.shell.expect(PROMPT)
|
|
358
403
|
return "---\n\nFailure: user interrupted the execution", 0.0
|
|
359
404
|
|
|
360
405
|
wait = timeout_s or TIMEOUT
|
|
361
|
-
index =
|
|
406
|
+
index = BASH_STATE.shell.expect([PROMPT, pexpect.TIMEOUT], timeout=wait)
|
|
362
407
|
if index == 1:
|
|
363
|
-
BASH_STATE
|
|
364
|
-
text =
|
|
408
|
+
BASH_STATE.set_pending()
|
|
409
|
+
text = BASH_STATE.shell.before or ""
|
|
365
410
|
|
|
366
411
|
text = render_terminal_output(text[-100_000:])
|
|
367
412
|
tokens = enc.encode(text)
|
|
@@ -386,11 +431,13 @@ Otherwise, you may want to try Ctrl-c again or program specific exit interactive
|
|
|
386
431
|
|
|
387
432
|
return text, 0
|
|
388
433
|
|
|
434
|
+
BASH_STATE.set_repl()
|
|
435
|
+
|
|
389
436
|
if is_interrupt:
|
|
390
437
|
return "Interrupt successful", 0.0
|
|
391
438
|
|
|
392
|
-
assert isinstance(
|
|
393
|
-
output = render_terminal_output(
|
|
439
|
+
assert isinstance(BASH_STATE.shell.before, str)
|
|
440
|
+
output = render_terminal_output(BASH_STATE.shell.before)
|
|
394
441
|
|
|
395
442
|
tokens = enc.encode(output)
|
|
396
443
|
if max_tokens and len(tokens) >= max_tokens:
|
|
@@ -404,8 +451,7 @@ Otherwise, you may want to try Ctrl-c again or program specific exit interactive
|
|
|
404
451
|
traceback.print_exc()
|
|
405
452
|
console.print("Malformed output, restarting shell", style="red")
|
|
406
453
|
# Malformed output, restart shell
|
|
407
|
-
|
|
408
|
-
SHELL = start_shell()
|
|
454
|
+
BASH_STATE.reset()
|
|
409
455
|
output = "(exit shell has restarted)"
|
|
410
456
|
return output, 0
|
|
411
457
|
|
|
@@ -449,8 +495,7 @@ T = TypeVar("T")
|
|
|
449
495
|
|
|
450
496
|
def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
451
497
|
def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> T:
|
|
452
|
-
|
|
453
|
-
if BASH_STATE == "pending":
|
|
498
|
+
if BASH_STATE.state == "pending":
|
|
454
499
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
455
500
|
|
|
456
501
|
return func(*args, **kwargs)
|
|
@@ -460,9 +505,9 @@ def ensure_no_previous_output(func: Callable[Param, T]) -> Callable[Param, T]:
|
|
|
460
505
|
|
|
461
506
|
def read_image_from_shell(file_path: str) -> ImageData:
|
|
462
507
|
if not os.path.isabs(file_path):
|
|
463
|
-
file_path = os.path.join(
|
|
508
|
+
file_path = os.path.join(BASH_STATE.cwd, file_path)
|
|
464
509
|
|
|
465
|
-
if not
|
|
510
|
+
if not BASH_STATE.is_in_docker:
|
|
466
511
|
if not os.path.exists(file_path):
|
|
467
512
|
raise ValueError(f"File {file_path} does not exist")
|
|
468
513
|
|
|
@@ -473,7 +518,9 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
473
518
|
return ImageData(media_type=image_type, data=image_b64) # type: ignore
|
|
474
519
|
else:
|
|
475
520
|
with TemporaryDirectory() as tmpdir:
|
|
476
|
-
rcode = os.system(
|
|
521
|
+
rcode = os.system(
|
|
522
|
+
f"docker cp {BASH_STATE.is_in_docker}:{shlex.quote(file_path)} {tmpdir}"
|
|
523
|
+
)
|
|
477
524
|
if rcode != 0:
|
|
478
525
|
raise Exception(f"Error: Read failed with code {rcode}")
|
|
479
526
|
path_ = os.path.join(tmpdir, os.path.basename(file_path))
|
|
@@ -486,11 +533,11 @@ def read_image_from_shell(file_path: str) -> ImageData:
|
|
|
486
533
|
|
|
487
534
|
def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
488
535
|
if not os.path.isabs(writefile.file_path):
|
|
489
|
-
return f"Failure: file_path should be absolute path, current working directory is {
|
|
536
|
+
return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
490
537
|
else:
|
|
491
538
|
path_ = writefile.file_path
|
|
492
539
|
|
|
493
|
-
if not
|
|
540
|
+
if not BASH_STATE.is_in_docker:
|
|
494
541
|
if error_on_exist and os.path.exists(path_):
|
|
495
542
|
file_data = Path(path_).read_text()
|
|
496
543
|
if file_data:
|
|
@@ -507,8 +554,8 @@ def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
|
507
554
|
else:
|
|
508
555
|
if error_on_exist:
|
|
509
556
|
# Check if it exists using os.system
|
|
510
|
-
cmd = f"test -f {path_}"
|
|
511
|
-
status = os.system(f'docker exec {
|
|
557
|
+
cmd = f"test -f {shlex.quote(path_)}"
|
|
558
|
+
status = os.system(f'docker exec {BASH_STATE.is_in_docker} bash -c "{cmd}"')
|
|
512
559
|
if status == 0:
|
|
513
560
|
return f"Error: can't write to existing file {path_}, use other functions to edit the file"
|
|
514
561
|
|
|
@@ -518,11 +565,15 @@ def write_file(writefile: CreateFileNew, error_on_exist: bool) -> str:
|
|
|
518
565
|
f.write(writefile.file_content)
|
|
519
566
|
os.chmod(tmppath, 0o777)
|
|
520
567
|
parent_dir = os.path.dirname(path_)
|
|
521
|
-
rcode = os.system(
|
|
568
|
+
rcode = os.system(
|
|
569
|
+
f"docker exec {BASH_STATE.is_in_docker} mkdir -p {parent_dir}"
|
|
570
|
+
)
|
|
522
571
|
if rcode != 0:
|
|
523
572
|
return f"Error: Write failed with code while creating dirs {rcode}"
|
|
524
573
|
|
|
525
|
-
rcode = os.system(
|
|
574
|
+
rcode = os.system(
|
|
575
|
+
f"docker cp {shlex.quote(tmppath)} {BASH_STATE.is_in_docker}:{shlex.quote(path_)}"
|
|
576
|
+
)
|
|
526
577
|
if rcode != 0:
|
|
527
578
|
return f"Error: Write failed with code {rcode}"
|
|
528
579
|
|
|
@@ -599,12 +650,12 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
599
650
|
|
|
600
651
|
if not os.path.isabs(fedit.file_path):
|
|
601
652
|
raise Exception(
|
|
602
|
-
f"Failure: file_path should be absolute path, current working directory is {
|
|
653
|
+
f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
603
654
|
)
|
|
604
655
|
else:
|
|
605
656
|
path_ = fedit.file_path
|
|
606
657
|
|
|
607
|
-
if not
|
|
658
|
+
if not BASH_STATE.is_in_docker:
|
|
608
659
|
if not os.path.exists(path_):
|
|
609
660
|
raise Exception(f"Error: file {path_} does not exist")
|
|
610
661
|
|
|
@@ -613,7 +664,9 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
613
664
|
else:
|
|
614
665
|
# Copy from docker
|
|
615
666
|
with TemporaryDirectory() as tmpdir:
|
|
616
|
-
rcode = os.system(
|
|
667
|
+
rcode = os.system(
|
|
668
|
+
f"docker cp {BASH_STATE.is_in_docker}:{shlex.quote(path_)} {tmpdir}"
|
|
669
|
+
)
|
|
617
670
|
if rcode != 0:
|
|
618
671
|
raise Exception(f"Error: Read failed with code {rcode}")
|
|
619
672
|
path_tmp = os.path.join(tmpdir, os.path.basename(path_))
|
|
@@ -666,7 +719,7 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
666
719
|
"Error: no valid search-replace blocks found, please check your syntax for FileEdit"
|
|
667
720
|
)
|
|
668
721
|
|
|
669
|
-
if not
|
|
722
|
+
if not BASH_STATE.is_in_docker:
|
|
670
723
|
with open(path_, "w") as f:
|
|
671
724
|
f.write(apply_diff_to)
|
|
672
725
|
else:
|
|
@@ -676,7 +729,9 @@ def do_diff_edit(fedit: FileEdit) -> str:
|
|
|
676
729
|
f.write(apply_diff_to)
|
|
677
730
|
os.chmod(path_tmp, 0o777)
|
|
678
731
|
# Copy to docker using docker cp
|
|
679
|
-
rcode = os.system(
|
|
732
|
+
rcode = os.system(
|
|
733
|
+
f"docker cp {shlex.quote(path_tmp)} {BASH_STATE.is_in_docker}:{shlex.quote(path_)}"
|
|
734
|
+
)
|
|
680
735
|
if rcode != 0:
|
|
681
736
|
raise Exception(f"Error: Write failed with code {rcode}")
|
|
682
737
|
|
|
@@ -870,7 +925,7 @@ def get_tool_output(
|
|
|
870
925
|
if imgBs64:
|
|
871
926
|
console.print("Captured screenshot")
|
|
872
927
|
outputs.append(ImageData(media_type="image/png", data=imgBs64))
|
|
873
|
-
if not
|
|
928
|
+
if not BASH_STATE.is_in_docker and isinstance(arg, GetScreenInfo):
|
|
874
929
|
try:
|
|
875
930
|
# At this point we should go into the docker env
|
|
876
931
|
res, _ = execute_bash(
|
|
@@ -896,7 +951,7 @@ def get_tool_output(
|
|
|
896
951
|
raise Exception(
|
|
897
952
|
f"Some error happened while going inside docker. I've reset the shell. Please start again. Error {e}"
|
|
898
953
|
)
|
|
899
|
-
|
|
954
|
+
BASH_STATE.set_in_docker(arg.docker_image_id)
|
|
900
955
|
return outputs, outputs_cost[1]
|
|
901
956
|
else:
|
|
902
957
|
raise ValueError(f"Unknown tool: {arg}")
|
|
@@ -1001,9 +1056,9 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
|
|
|
1001
1056
|
console.print(f"Reading file: {readfile.file_path}")
|
|
1002
1057
|
|
|
1003
1058
|
if not os.path.isabs(readfile.file_path):
|
|
1004
|
-
return f"Failure: file_path should be absolute path, current working directory is {
|
|
1059
|
+
return f"Failure: file_path should be absolute path, current working directory is {BASH_STATE.cwd}"
|
|
1005
1060
|
|
|
1006
|
-
if not
|
|
1061
|
+
if not BASH_STATE.is_in_docker:
|
|
1007
1062
|
path = Path(readfile.file_path)
|
|
1008
1063
|
if not path.exists():
|
|
1009
1064
|
return f"Error: file {readfile.file_path} does not exist"
|
|
@@ -1013,7 +1068,8 @@ def read_file(readfile: ReadFile, max_tokens: Optional[int]) -> str:
|
|
|
1013
1068
|
|
|
1014
1069
|
else:
|
|
1015
1070
|
return_code, content, stderr = command_run(
|
|
1016
|
-
f"cat {readfile.file_path}",
|
|
1071
|
+
f"docker exec {BASH_STATE.is_in_docker} cat {shlex.quote(readfile.file_path)}",
|
|
1072
|
+
timeout=TIMEOUT,
|
|
1017
1073
|
)
|
|
1018
1074
|
if return_code != 0:
|
|
1019
1075
|
raise Exception(
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.1
|
|
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=kokbgxxBikIK39moSeg5uGvtBUzLXHnP5-d4jywZYRQ,35762
|
|
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.
|
|
20
|
-
wcgw-2.
|
|
21
|
-
wcgw-2.
|
|
22
|
-
wcgw-2.
|
|
19
|
+
wcgw-2.1.1.dist-info/METADATA,sha256=T6tzEXw6jTT0KpDoJmYa6QQkRl1xRB2E4VJg0YwITv4,7421
|
|
20
|
+
wcgw-2.1.1.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
21
|
+
wcgw-2.1.1.dist-info/entry_points.txt,sha256=eKo1omwbAggWlQ0l7GKoR7uV1-j16nk9tK0BhC2Oz_E,120
|
|
22
|
+
wcgw-2.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|