wcgw 3.0.1rc3__py3-none-any.whl → 3.0.3__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/bash_state/bash_state.py +40 -117
- wcgw/client/modes.py +13 -12
- wcgw/client/tool_prompts.py +9 -15
- wcgw/client/tools.py +120 -50
- wcgw/relay/serve.py +0 -31
- wcgw/types_.py +24 -20
- {wcgw-3.0.1rc3.dist-info → wcgw-3.0.3.dist-info}/METADATA +25 -8
- {wcgw-3.0.1rc3.dist-info → wcgw-3.0.3.dist-info}/RECORD +13 -13
- wcgw_cli/anthropic_client.py +26 -10
- wcgw_cli/openai_client.py +1 -0
- {wcgw-3.0.1rc3.dist-info → wcgw-3.0.3.dist-info}/WHEEL +0 -0
- {wcgw-3.0.1rc3.dist-info → wcgw-3.0.3.dist-info}/entry_points.txt +0 -0
- {wcgw-3.0.1rc3.dist-info → wcgw-3.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -8,8 +8,6 @@ import traceback
|
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from typing import (
|
|
10
10
|
Any,
|
|
11
|
-
Callable,
|
|
12
|
-
Concatenate,
|
|
13
11
|
Literal,
|
|
14
12
|
Optional,
|
|
15
13
|
ParamSpec,
|
|
@@ -107,6 +105,9 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
107
105
|
output = (e.stdout or "") + (e.stderr or "")
|
|
108
106
|
except FileNotFoundError:
|
|
109
107
|
return
|
|
108
|
+
except Exception as e:
|
|
109
|
+
console.log(f"{e}: exception while clearing running screens.")
|
|
110
|
+
return
|
|
110
111
|
|
|
111
112
|
sessions_to_kill = []
|
|
112
113
|
|
|
@@ -130,8 +131,8 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
130
131
|
check=True,
|
|
131
132
|
timeout=CONFIG.timeout,
|
|
132
133
|
)
|
|
133
|
-
except
|
|
134
|
-
console.log(f"Failed to kill screen session: {session}")
|
|
134
|
+
except Exception as e:
|
|
135
|
+
console.log(f"Failed to kill screen session: {session}\n{e}")
|
|
135
136
|
|
|
136
137
|
|
|
137
138
|
def start_shell(
|
|
@@ -217,33 +218,7 @@ P = ParamSpec("P")
|
|
|
217
218
|
R = TypeVar("R")
|
|
218
219
|
|
|
219
220
|
|
|
220
|
-
def requires_shell(
|
|
221
|
-
func: Callable[Concatenate["BashState", "pexpect.spawn[str]", P], R],
|
|
222
|
-
) -> Callable[Concatenate["BashState", P], R]:
|
|
223
|
-
def wrapper(self: "BashState", /, *args: P.args, **kwargs: P.kwargs) -> R:
|
|
224
|
-
if not self._shell_loading.is_set():
|
|
225
|
-
if not self._shell_loading.wait(
|
|
226
|
-
timeout=CONFIG.timeout * 2
|
|
227
|
-
): # Twice in worst case if screen fails
|
|
228
|
-
raise RuntimeError("Shell initialization timeout")
|
|
229
|
-
|
|
230
|
-
if self._shell_error:
|
|
231
|
-
raise RuntimeError(f"Shell failed to initialize: {self._shell_error}.")
|
|
232
|
-
|
|
233
|
-
if not self._shell:
|
|
234
|
-
raise RuntimeError("Shell not initialized")
|
|
235
|
-
|
|
236
|
-
return func(self, self._shell, *args, **kwargs)
|
|
237
|
-
|
|
238
|
-
return wrapper
|
|
239
|
-
|
|
240
|
-
|
|
241
221
|
class BashState:
|
|
242
|
-
_shell: Optional["pexpect.spawn[str]"]
|
|
243
|
-
_shell_id: Optional[str]
|
|
244
|
-
_shell_lock: threading.Lock
|
|
245
|
-
_shell_loading: threading.Event
|
|
246
|
-
_shell_error: Optional[Exception]
|
|
247
222
|
_use_screen: bool
|
|
248
223
|
|
|
249
224
|
def __init__(
|
|
@@ -266,67 +241,39 @@ class BashState:
|
|
|
266
241
|
self._write_if_empty_mode: WriteIfEmptyMode = (
|
|
267
242
|
write_if_empty_mode or WriteIfEmptyMode("all")
|
|
268
243
|
)
|
|
269
|
-
self._mode = mode or
|
|
244
|
+
self._mode = mode or "wcgw"
|
|
270
245
|
self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
|
|
271
246
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
272
247
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
273
|
-
self._shell = None
|
|
274
|
-
self._shell_id = None
|
|
275
|
-
self._shell_lock = threading.Lock()
|
|
276
|
-
self._shell_loading = threading.Event()
|
|
277
|
-
self._shell_error = None
|
|
278
248
|
self._use_screen = use_screen
|
|
279
|
-
self.
|
|
249
|
+
self._init_shell()
|
|
280
250
|
|
|
281
|
-
def
|
|
282
|
-
def load_shell() -> None:
|
|
283
|
-
try:
|
|
284
|
-
with self._shell_lock:
|
|
285
|
-
if self._shell is not None:
|
|
286
|
-
return
|
|
287
|
-
self._init_shell()
|
|
288
|
-
self.run_bg_expect_thread()
|
|
289
|
-
except Exception as e:
|
|
290
|
-
self._shell_error = e
|
|
291
|
-
finally:
|
|
292
|
-
self._shell_loading.set()
|
|
293
|
-
|
|
294
|
-
threading.Thread(target=load_shell).start()
|
|
295
|
-
|
|
296
|
-
@requires_shell
|
|
297
|
-
def expect(
|
|
298
|
-
self, shell: "pexpect.spawn[str]", pattern: Any, timeout: Optional[float] = -1
|
|
299
|
-
) -> int:
|
|
251
|
+
def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
|
|
300
252
|
self.close_bg_expect_thread()
|
|
301
|
-
output =
|
|
253
|
+
output = self._shell.expect(pattern, timeout)
|
|
302
254
|
return output
|
|
303
255
|
|
|
304
|
-
|
|
305
|
-
def send(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
|
|
256
|
+
def send(self, s: str | bytes) -> int:
|
|
306
257
|
self.close_bg_expect_thread()
|
|
307
|
-
output =
|
|
258
|
+
output = self._shell.send(s)
|
|
308
259
|
return output
|
|
309
260
|
|
|
310
|
-
|
|
311
|
-
def sendline(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
|
|
261
|
+
def sendline(self, s: str | bytes) -> int:
|
|
312
262
|
self.close_bg_expect_thread()
|
|
313
|
-
output =
|
|
263
|
+
output = self._shell.sendline(s)
|
|
314
264
|
return output
|
|
315
265
|
|
|
316
266
|
@property
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return shell.linesep
|
|
267
|
+
def linesep(self) -> Any:
|
|
268
|
+
return self._shell.linesep
|
|
320
269
|
|
|
321
|
-
|
|
322
|
-
def sendintr(self, shell: "pexpect.spawn[str]") -> None:
|
|
270
|
+
def sendintr(self) -> None:
|
|
323
271
|
self.close_bg_expect_thread()
|
|
324
|
-
|
|
272
|
+
self._shell.sendintr()
|
|
325
273
|
|
|
326
274
|
@property
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return shell.before
|
|
275
|
+
def before(self) -> Optional[str]:
|
|
276
|
+
return self._shell.before
|
|
330
277
|
|
|
331
278
|
def run_bg_expect_thread(self) -> None:
|
|
332
279
|
"""
|
|
@@ -337,16 +284,11 @@ class BashState:
|
|
|
337
284
|
while True:
|
|
338
285
|
if self._bg_expect_thread_stop_event.is_set():
|
|
339
286
|
break
|
|
340
|
-
if self._shell is None:
|
|
341
|
-
time.sleep(0.1)
|
|
342
|
-
continue
|
|
343
287
|
output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
|
|
344
288
|
if output == 0:
|
|
345
289
|
break
|
|
346
290
|
|
|
347
|
-
if self._bg_expect_thread
|
|
348
|
-
if not self._bg_expect_thread_stop_event.is_set():
|
|
349
|
-
return
|
|
291
|
+
if self._bg_expect_thread:
|
|
350
292
|
self.close_bg_expect_thread()
|
|
351
293
|
|
|
352
294
|
self._bg_expect_thread = threading.Thread(
|
|
@@ -363,13 +305,8 @@ class BashState:
|
|
|
363
305
|
|
|
364
306
|
def cleanup(self) -> None:
|
|
365
307
|
self.close_bg_expect_thread()
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
self._shell.close(True)
|
|
369
|
-
if self._shell_id:
|
|
370
|
-
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
371
|
-
self._shell = None
|
|
372
|
-
self._shell_id = None
|
|
308
|
+
self._shell.close(True)
|
|
309
|
+
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
373
310
|
|
|
374
311
|
def __enter__(self) -> "BashState":
|
|
375
312
|
return self
|
|
@@ -393,31 +330,24 @@ class BashState:
|
|
|
393
330
|
def write_if_empty_mode(self) -> WriteIfEmptyMode:
|
|
394
331
|
return self._write_if_empty_mode
|
|
395
332
|
|
|
396
|
-
|
|
397
|
-
def ensure_env_and_bg_jobs(self, _: "pexpect.spawn[str]") -> Optional[int]:
|
|
398
|
-
return self._ensure_env_and_bg_jobs()
|
|
399
|
-
|
|
400
|
-
def _ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
401
|
-
# Do not add @requires_shell decorator here, as it will cause deadlock
|
|
402
|
-
self.close_bg_expect_thread()
|
|
403
|
-
assert self._shell is not None, "Bad state, shell is not initialized"
|
|
333
|
+
def ensure_env_and_bg_jobs(self) -> Optional[int]:
|
|
404
334
|
quick_timeout = 0.2 if not self.over_screen else 1
|
|
405
335
|
# First reset the prompt in case venv was sourced or other reasons.
|
|
406
|
-
self.
|
|
407
|
-
self.
|
|
336
|
+
self.sendline(PROMPT_STATEMENT)
|
|
337
|
+
self.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
408
338
|
# Reset echo also if it was enabled
|
|
409
339
|
command = "jobs | wc -l"
|
|
410
|
-
self.
|
|
340
|
+
self.sendline(command)
|
|
411
341
|
before = ""
|
|
412
342
|
counts = 0
|
|
413
343
|
while not _is_int(before): # Consume all previous output
|
|
414
344
|
try:
|
|
415
|
-
self.
|
|
345
|
+
self.expect(PROMPT_CONST, timeout=quick_timeout)
|
|
416
346
|
except pexpect.TIMEOUT:
|
|
417
347
|
self.console.print(f"Couldn't get exit code, before: {before}")
|
|
418
348
|
raise
|
|
419
349
|
|
|
420
|
-
before_val = self.
|
|
350
|
+
before_val = self.before
|
|
421
351
|
if not isinstance(before_val, str):
|
|
422
352
|
before_val = str(before_val)
|
|
423
353
|
assert isinstance(before_val, str)
|
|
@@ -435,7 +365,6 @@ class BashState:
|
|
|
435
365
|
|
|
436
366
|
def _init_shell(self) -> None:
|
|
437
367
|
self._state: Literal["repl"] | datetime.datetime = "repl"
|
|
438
|
-
self._is_in_docker: Optional[str] = ""
|
|
439
368
|
# Ensure self._cwd exists
|
|
440
369
|
os.makedirs(self._cwd, exist_ok=True)
|
|
441
370
|
try:
|
|
@@ -461,10 +390,12 @@ class BashState:
|
|
|
461
390
|
|
|
462
391
|
self._pending_output = ""
|
|
463
392
|
try:
|
|
464
|
-
self.
|
|
393
|
+
self.ensure_env_and_bg_jobs()
|
|
465
394
|
except ValueError as e:
|
|
466
395
|
self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
|
|
467
396
|
|
|
397
|
+
self.run_bg_expect_thread()
|
|
398
|
+
|
|
468
399
|
def set_pending(self, last_pending_output: str) -> None:
|
|
469
400
|
if not isinstance(self._state, datetime.datetime):
|
|
470
401
|
self._state = datetime.datetime.now()
|
|
@@ -480,13 +411,6 @@ class BashState:
|
|
|
480
411
|
return "repl"
|
|
481
412
|
return "pending"
|
|
482
413
|
|
|
483
|
-
@property
|
|
484
|
-
def is_in_docker(self) -> Optional[str]:
|
|
485
|
-
return self._is_in_docker
|
|
486
|
-
|
|
487
|
-
def set_in_docker(self, docker_image_id: str) -> None:
|
|
488
|
-
self._is_in_docker = docker_image_id
|
|
489
|
-
|
|
490
414
|
@property
|
|
491
415
|
def cwd(self) -> str:
|
|
492
416
|
return self._cwd
|
|
@@ -495,23 +419,22 @@ class BashState:
|
|
|
495
419
|
def prompt(self) -> str:
|
|
496
420
|
return PROMPT_CONST
|
|
497
421
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
before_val = shell.before
|
|
422
|
+
def update_cwd(self) -> str:
|
|
423
|
+
self.sendline("pwd")
|
|
424
|
+
self.expect(PROMPT_CONST, timeout=0.2)
|
|
425
|
+
before_val = self.before
|
|
503
426
|
if not isinstance(before_val, str):
|
|
504
427
|
before_val = str(before_val)
|
|
505
428
|
before_lines = render_terminal_output(before_val)
|
|
506
429
|
current_dir = "\n".join(before_lines).strip()
|
|
430
|
+
if current_dir.startswith("pwd"):
|
|
431
|
+
current_dir = current_dir[3:].strip()
|
|
507
432
|
self._cwd = current_dir
|
|
508
433
|
return current_dir
|
|
509
434
|
|
|
510
435
|
def reset_shell(self) -> None:
|
|
511
436
|
self.cleanup()
|
|
512
|
-
self.
|
|
513
|
-
self._shell_error = None
|
|
514
|
-
self._start_shell_loading()
|
|
437
|
+
self._init_shell()
|
|
515
438
|
|
|
516
439
|
def serialize(self) -> dict[str, Any]:
|
|
517
440
|
"""Serialize BashState to a dictionary for saving"""
|
|
@@ -531,7 +454,7 @@ class BashState:
|
|
|
531
454
|
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
532
455
|
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
533
456
|
WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
|
|
534
|
-
|
|
457
|
+
state["mode"],
|
|
535
458
|
state["whitelist_for_overwrite"],
|
|
536
459
|
)
|
|
537
460
|
|
|
@@ -595,7 +518,7 @@ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run mul
|
|
|
595
518
|
1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
|
|
596
519
|
2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
|
|
597
520
|
3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
|
|
598
|
-
4.
|
|
521
|
+
4. Interrupt and run the process in background by re-running it using screen
|
|
599
522
|
"""
|
|
600
523
|
|
|
601
524
|
|
wcgw/client/modes.py
CHANGED
|
@@ -85,7 +85,7 @@ You are now running in "code_writer" mode.
|
|
|
85
85
|
base += path_prompt
|
|
86
86
|
|
|
87
87
|
run_command_common = """
|
|
88
|
-
- Do not use Ctrl-c
|
|
88
|
+
- Do not use Ctrl-c interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
89
89
|
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
90
90
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
91
91
|
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
@@ -124,7 +124,7 @@ Instructions:
|
|
|
124
124
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
125
125
|
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
126
126
|
- Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using wcgw tools
|
|
127
|
-
- Do not use Ctrl-c or
|
|
127
|
+
- Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
128
128
|
- Do not use echo to write multi-line files, always use FileEdit tool to update a code.
|
|
129
129
|
|
|
130
130
|
Additional instructions:
|
|
@@ -138,10 +138,11 @@ ARCHITECT_PROMPT = """You are now running in "architect" mode. This means
|
|
|
138
138
|
- You are not allowed to edit or update any file. You are not allowed to create any file.
|
|
139
139
|
- You are not allowed to run any commands that may change disk, system configuration, packages or environment. Only read-only commands are allowed.
|
|
140
140
|
- Only run commands that allows you to explore the repository, understand the system or read anything of relevance.
|
|
141
|
-
- Do not use Ctrl-c or
|
|
141
|
+
- Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
|
|
142
142
|
- You are not allowed to change directory (bash will run in -r mode)
|
|
143
|
+
- Share only snippets when any implementation is requested.
|
|
143
144
|
|
|
144
|
-
|
|
145
|
+
Respond only after doing the following:
|
|
145
146
|
- Read as many relevant files as possible.
|
|
146
147
|
- Be comprehensive in your understanding and search of relevant files.
|
|
147
148
|
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
@@ -149,17 +150,17 @@ Your response should be in self-critique and brainstorm style.
|
|
|
149
150
|
|
|
150
151
|
|
|
151
152
|
DEFAULT_MODES: dict[Modes, ModeImpl] = {
|
|
152
|
-
|
|
153
|
+
"wcgw": ModeImpl(
|
|
153
154
|
bash_command_mode=BashCommandMode("normal_mode", "all"),
|
|
154
155
|
write_if_empty_mode=WriteIfEmptyMode("all"),
|
|
155
156
|
file_edit_mode=FileEditMode("all"),
|
|
156
157
|
),
|
|
157
|
-
|
|
158
|
+
"architect": ModeImpl(
|
|
158
159
|
bash_command_mode=BashCommandMode("restricted_mode", "all"),
|
|
159
160
|
write_if_empty_mode=WriteIfEmptyMode([]),
|
|
160
161
|
file_edit_mode=FileEditMode([]),
|
|
161
162
|
),
|
|
162
|
-
|
|
163
|
+
"code_writer": ModeImpl(
|
|
163
164
|
bash_command_mode=BashCommandMode("normal_mode", "all"),
|
|
164
165
|
write_if_empty_mode=WriteIfEmptyMode("all"),
|
|
165
166
|
file_edit_mode=FileEditMode("all"),
|
|
@@ -172,11 +173,11 @@ def modes_to_state(
|
|
|
172
173
|
) -> tuple[BashCommandMode, FileEditMode, WriteIfEmptyMode, Modes]:
|
|
173
174
|
# First get default mode config
|
|
174
175
|
if isinstance(mode, str):
|
|
175
|
-
mode_impl = DEFAULT_MODES[
|
|
176
|
-
mode_name =
|
|
176
|
+
mode_impl = DEFAULT_MODES[mode] # converts str to Modes enum
|
|
177
|
+
mode_name: Modes = mode
|
|
177
178
|
else:
|
|
178
179
|
# For CodeWriterMode, use code_writer as base and override
|
|
179
|
-
mode_impl = DEFAULT_MODES[
|
|
180
|
+
mode_impl = DEFAULT_MODES["code_writer"]
|
|
180
181
|
# Override with custom settings from CodeWriterMode
|
|
181
182
|
mode_impl = ModeImpl(
|
|
182
183
|
bash_command_mode=BashCommandMode(
|
|
@@ -186,7 +187,7 @@ def modes_to_state(
|
|
|
186
187
|
file_edit_mode=FileEditMode(mode.allowed_globs),
|
|
187
188
|
write_if_empty_mode=WriteIfEmptyMode(mode.allowed_globs),
|
|
188
189
|
)
|
|
189
|
-
mode_name =
|
|
190
|
+
mode_name = "code_writer"
|
|
190
191
|
return (
|
|
191
192
|
mode_impl.bash_command_mode,
|
|
192
193
|
mode_impl.file_edit_mode,
|
|
@@ -233,4 +234,4 @@ Provide all relevant file paths in order to understand and solve the the task. E
|
|
|
233
234
|
(Note to self: this conversation can then be resumed later asking "Resume wcgw task `<generated id>`" which should call Initialize tool)
|
|
234
235
|
"""
|
|
235
236
|
|
|
236
|
-
KTS = {
|
|
237
|
+
KTS = {"wcgw": WCGW_KT, "architect": ARCHITECT_KT, "code_writer": WCGW_KT}
|
wcgw/client/tool_prompts.py
CHANGED
|
@@ -9,7 +9,6 @@ from ..types_ import (
|
|
|
9
9
|
Initialize,
|
|
10
10
|
ReadFiles,
|
|
11
11
|
ReadImage,
|
|
12
|
-
ResetWcgw,
|
|
13
12
|
WriteIfEmpty,
|
|
14
13
|
)
|
|
15
14
|
|
|
@@ -30,15 +29,15 @@ TOOL_PROMPTS = [
|
|
|
30
29
|
name="Initialize",
|
|
31
30
|
description="""
|
|
32
31
|
- Always call this at the start of the conversation before using any of the shell tools from wcgw.
|
|
33
|
-
- This will reset the shell.
|
|
34
32
|
- Use `any_workspace_path` to initialize the shell in the appropriate project directory.
|
|
35
|
-
- If the user has mentioned a workspace or project root
|
|
36
|
-
- If
|
|
37
|
-
- If user has mentioned any files use `initial_files_to_read` to read, use absolute paths only.
|
|
38
|
-
- Leave `any_workspace_path` as empty if no file or folder is mentioned.
|
|
33
|
+
- If the user has mentioned a workspace or project root or any other file or folder use it to set `any_workspace_path`.
|
|
34
|
+
- If user has mentioned any files use `initial_files_to_read` to read, use absolute paths only (~ allowed)
|
|
39
35
|
- By default use mode "wcgw"
|
|
40
36
|
- In "code-writer" mode, set the commands and globs which user asked to set, otherwise use 'all'.
|
|
41
|
-
-
|
|
37
|
+
- Use type="first_call" if it's the first call to this tool.
|
|
38
|
+
- Use type="user_asked_mode_change" if in a conversation user has asked to change mode.
|
|
39
|
+
- Use type="reset_shell" if in a conversation shell is not working after multiple tries.
|
|
40
|
+
- Use type="user_asked_change_workspace" if in a conversation user asked to change workspace
|
|
42
41
|
""",
|
|
43
42
|
),
|
|
44
43
|
Prompts(
|
|
@@ -62,7 +61,7 @@ TOOL_PROMPTS = [
|
|
|
62
61
|
name="ReadFiles",
|
|
63
62
|
description="""
|
|
64
63
|
- Read full file content of one or more files.
|
|
65
|
-
- Provide absolute
|
|
64
|
+
- Provide absolute paths only (~ allowed)
|
|
66
65
|
""",
|
|
67
66
|
),
|
|
68
67
|
Prompts(
|
|
@@ -70,7 +69,7 @@ TOOL_PROMPTS = [
|
|
|
70
69
|
name="WriteIfEmpty",
|
|
71
70
|
description="""
|
|
72
71
|
- Write content to an empty or non-existent file. Provide file path and content. Use this instead of BashCommand for writing new files.
|
|
73
|
-
- Provide absolute
|
|
72
|
+
- Provide absolute path only.
|
|
74
73
|
- For editing existing files, use FileEdit instead of this tool.
|
|
75
74
|
""",
|
|
76
75
|
),
|
|
@@ -79,16 +78,11 @@ TOOL_PROMPTS = [
|
|
|
79
78
|
name="ReadImage",
|
|
80
79
|
description="Read an image from the shell.",
|
|
81
80
|
),
|
|
82
|
-
Prompts(
|
|
83
|
-
inputSchema=ResetWcgw.model_json_schema(),
|
|
84
|
-
name="ResetWcgw",
|
|
85
|
-
description="Resets the shell. Use either when changing mode, or when all interrupts and prompt reset attempts have failed repeatedly.",
|
|
86
|
-
),
|
|
87
81
|
Prompts(
|
|
88
82
|
inputSchema=FileEdit.model_json_schema(),
|
|
89
83
|
name="FileEdit",
|
|
90
84
|
description="""
|
|
91
|
-
- Use absolute
|
|
85
|
+
- Use absolute path only.
|
|
92
86
|
- Use SEARCH/REPLACE blocks to edit the file.
|
|
93
87
|
- If the edit fails due to block not matching, please retry with correct block till it matches. Re-read the file to ensure you've all the lines correct.
|
|
94
88
|
"""
|
wcgw/client/tools.py
CHANGED
|
@@ -4,6 +4,7 @@ import glob
|
|
|
4
4
|
import json
|
|
5
5
|
import mimetypes
|
|
6
6
|
import os
|
|
7
|
+
import subprocess
|
|
7
8
|
import traceback
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
from os.path import expanduser
|
|
@@ -38,7 +39,6 @@ from ..types_ import (
|
|
|
38
39
|
ModesConfig,
|
|
39
40
|
ReadFiles,
|
|
40
41
|
ReadImage,
|
|
41
|
-
ResetWcgw,
|
|
42
42
|
WriteIfEmpty,
|
|
43
43
|
)
|
|
44
44
|
from .bash_state.bash_state import (
|
|
@@ -68,13 +68,13 @@ INITIALIZED = False
|
|
|
68
68
|
|
|
69
69
|
def get_mode_prompt(context: Context) -> str:
|
|
70
70
|
mode_prompt = ""
|
|
71
|
-
if context.bash_state.mode ==
|
|
71
|
+
if context.bash_state.mode == "code_writer":
|
|
72
72
|
mode_prompt = code_writer_prompt(
|
|
73
73
|
context.bash_state.file_edit_mode.allowed_globs,
|
|
74
74
|
context.bash_state.write_if_empty_mode.allowed_globs,
|
|
75
75
|
"all" if context.bash_state.bash_command_mode.allowed_commands else [],
|
|
76
76
|
)
|
|
77
|
-
elif context.bash_state.mode ==
|
|
77
|
+
elif context.bash_state.mode == "architect":
|
|
78
78
|
mode_prompt = ARCHITECT_PROMPT
|
|
79
79
|
else:
|
|
80
80
|
mode_prompt = WCGW_PROMPT
|
|
@@ -83,6 +83,7 @@ def get_mode_prompt(context: Context) -> str:
|
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def initialize(
|
|
86
|
+
type: Literal["user_asked_change_workspace", "first_call"],
|
|
86
87
|
context: Context,
|
|
87
88
|
any_workspace_path: str,
|
|
88
89
|
read_files_: list[str],
|
|
@@ -96,20 +97,26 @@ def initialize(
|
|
|
96
97
|
|
|
97
98
|
memory = ""
|
|
98
99
|
loaded_state = None
|
|
99
|
-
if task_id_to_resume:
|
|
100
|
-
try:
|
|
101
|
-
project_root_path, task_mem, loaded_state = load_memory(
|
|
102
|
-
task_id_to_resume,
|
|
103
|
-
max_tokens,
|
|
104
|
-
lambda x: default_enc.encoder(x),
|
|
105
|
-
lambda x: default_enc.decoder(x),
|
|
106
|
-
)
|
|
107
|
-
memory = "Following is the retrieved task:\n" + task_mem
|
|
108
|
-
if os.path.exists(project_root_path):
|
|
109
|
-
any_workspace_path = project_root_path
|
|
110
100
|
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
if type == "first_call":
|
|
102
|
+
if task_id_to_resume:
|
|
103
|
+
try:
|
|
104
|
+
project_root_path, task_mem, loaded_state = load_memory(
|
|
105
|
+
task_id_to_resume,
|
|
106
|
+
max_tokens,
|
|
107
|
+
lambda x: default_enc.encoder(x),
|
|
108
|
+
lambda x: default_enc.decoder(x),
|
|
109
|
+
)
|
|
110
|
+
memory = "Following is the retrieved task:\n" + task_mem
|
|
111
|
+
if os.path.exists(project_root_path):
|
|
112
|
+
any_workspace_path = project_root_path
|
|
113
|
+
|
|
114
|
+
except Exception:
|
|
115
|
+
memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
|
|
116
|
+
elif task_id_to_resume:
|
|
117
|
+
memory = (
|
|
118
|
+
"Warning: task can only be resumed in a new conversation. No task loaded."
|
|
119
|
+
)
|
|
113
120
|
|
|
114
121
|
folder_to_start = None
|
|
115
122
|
if any_workspace_path:
|
|
@@ -165,7 +172,9 @@ def initialize(
|
|
|
165
172
|
context.console.print(traceback.format_exc())
|
|
166
173
|
context.console.print("Error: couldn't load bash state")
|
|
167
174
|
pass
|
|
175
|
+
mode_prompt = get_mode_prompt(context)
|
|
168
176
|
else:
|
|
177
|
+
mode_changed = is_mode_change(mode, context.bash_state)
|
|
169
178
|
state = modes_to_state(mode)
|
|
170
179
|
context.bash_state.load_state(
|
|
171
180
|
state[0],
|
|
@@ -175,6 +184,11 @@ def initialize(
|
|
|
175
184
|
list(context.bash_state.whitelist_for_overwrite),
|
|
176
185
|
str(folder_to_start) if folder_to_start else "",
|
|
177
186
|
)
|
|
187
|
+
if type == "first_call" or mode_changed:
|
|
188
|
+
mode_prompt = get_mode_prompt(context)
|
|
189
|
+
else:
|
|
190
|
+
mode_prompt = ""
|
|
191
|
+
|
|
178
192
|
del mode
|
|
179
193
|
|
|
180
194
|
initial_files_context = ""
|
|
@@ -189,7 +203,7 @@ def initialize(
|
|
|
189
203
|
|
|
190
204
|
uname_sysname = os.uname().sysname
|
|
191
205
|
uname_machine = os.uname().machine
|
|
192
|
-
|
|
206
|
+
|
|
193
207
|
output = f"""
|
|
194
208
|
{mode_prompt}
|
|
195
209
|
|
|
@@ -212,20 +226,34 @@ Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
|
212
226
|
return output, context
|
|
213
227
|
|
|
214
228
|
|
|
215
|
-
def
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
def is_mode_change(mode_config: ModesConfig, bash_state: BashState) -> bool:
|
|
230
|
+
allowed = modes_to_state(mode_config)
|
|
231
|
+
bash_allowed = (
|
|
232
|
+
bash_state.bash_command_mode,
|
|
233
|
+
bash_state.file_edit_mode,
|
|
234
|
+
bash_state.write_if_empty_mode,
|
|
235
|
+
bash_state.mode,
|
|
236
|
+
)
|
|
237
|
+
return allowed != bash_allowed
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def reset_wcgw(
|
|
241
|
+
context: Context,
|
|
242
|
+
starting_directory: str,
|
|
243
|
+
mode_name: Optional[Modes],
|
|
244
|
+
change_mode: ModesConfig,
|
|
245
|
+
) -> str:
|
|
246
|
+
global INITIALIZED
|
|
247
|
+
if mode_name:
|
|
248
|
+
# update modes if they're relative
|
|
249
|
+
if isinstance(change_mode, CodeWriterMode):
|
|
250
|
+
change_mode.update_relative_globs(starting_directory)
|
|
223
251
|
else:
|
|
224
|
-
|
|
252
|
+
assert isinstance(change_mode, str)
|
|
225
253
|
|
|
226
254
|
# Get new state configuration
|
|
227
255
|
bash_command_mode, file_edit_mode, write_if_empty_mode, mode = modes_to_state(
|
|
228
|
-
|
|
256
|
+
change_mode
|
|
229
257
|
)
|
|
230
258
|
|
|
231
259
|
# Reset shell with new mode
|
|
@@ -235,11 +263,12 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
|
|
|
235
263
|
write_if_empty_mode,
|
|
236
264
|
mode,
|
|
237
265
|
list(context.bash_state.whitelist_for_overwrite),
|
|
238
|
-
|
|
266
|
+
starting_directory,
|
|
239
267
|
)
|
|
240
268
|
mode_prompt = get_mode_prompt(context)
|
|
269
|
+
INITIALIZED = True
|
|
241
270
|
return (
|
|
242
|
-
f"Reset successful with mode change to {
|
|
271
|
+
f"Reset successful with mode change to {mode_name}.\n"
|
|
243
272
|
+ mode_prompt
|
|
244
273
|
+ "\n"
|
|
245
274
|
+ get_status(context.bash_state)
|
|
@@ -258,9 +287,8 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
|
|
|
258
287
|
write_if_empty_mode,
|
|
259
288
|
mode,
|
|
260
289
|
list(context.bash_state.whitelist_for_overwrite),
|
|
261
|
-
|
|
290
|
+
starting_directory,
|
|
262
291
|
)
|
|
263
|
-
global INITIALIZED
|
|
264
292
|
INITIALIZED = True
|
|
265
293
|
return "Reset successful" + get_status(context.bash_state)
|
|
266
294
|
|
|
@@ -281,6 +309,30 @@ def expand_user(path: str) -> str:
|
|
|
281
309
|
return expanduser(path)
|
|
282
310
|
|
|
283
311
|
|
|
312
|
+
def try_open_file(file_path: str) -> None:
|
|
313
|
+
"""Try to open a file using the system's default application."""
|
|
314
|
+
# Determine the appropriate open command based on OS
|
|
315
|
+
open_cmd = None
|
|
316
|
+
if os.uname().sysname == "Darwin": # macOS
|
|
317
|
+
open_cmd = "open"
|
|
318
|
+
elif os.uname().sysname == "Linux":
|
|
319
|
+
# Try common Linux open commands
|
|
320
|
+
for cmd in ["xdg-open", "gnome-open", "kde-open"]:
|
|
321
|
+
try:
|
|
322
|
+
subprocess.run(["which", cmd], timeout=1, capture_output=True)
|
|
323
|
+
open_cmd = cmd
|
|
324
|
+
break
|
|
325
|
+
except:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
# Try to open the file if a command is available
|
|
329
|
+
if open_cmd:
|
|
330
|
+
try:
|
|
331
|
+
subprocess.run([open_cmd, file_path], timeout=2)
|
|
332
|
+
except:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
|
|
284
336
|
MEDIA_TYPES = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
|
|
285
337
|
|
|
286
338
|
|
|
@@ -514,7 +566,6 @@ Syntax errors:
|
|
|
514
566
|
|
|
515
567
|
TOOLS = (
|
|
516
568
|
BashCommand
|
|
517
|
-
| ResetWcgw
|
|
518
569
|
| WriteIfEmpty
|
|
519
570
|
| FileEdit
|
|
520
571
|
| ReadImage
|
|
@@ -532,8 +583,6 @@ def which_tool(args: str) -> TOOLS:
|
|
|
532
583
|
def which_tool_name(name: str) -> Type[TOOLS]:
|
|
533
584
|
if name == "BashCommand":
|
|
534
585
|
return BashCommand
|
|
535
|
-
elif name == "ResetWcgw":
|
|
536
|
-
return ResetWcgw
|
|
537
586
|
elif name == "WriteIfEmpty":
|
|
538
587
|
return WriteIfEmpty
|
|
539
588
|
elif name == "FileEdit":
|
|
@@ -613,26 +662,43 @@ def get_tool_output(
|
|
|
613
662
|
elif isinstance(arg, ReadFiles):
|
|
614
663
|
context.console.print("Calling read file tool")
|
|
615
664
|
output = read_files(arg.file_paths, max_tokens, context), 0.0
|
|
616
|
-
elif isinstance(arg, ResetWcgw):
|
|
617
|
-
context.console.print("Calling reset wcgw tool")
|
|
618
|
-
output = reset_wcgw(context, arg), 0.0
|
|
619
|
-
|
|
620
665
|
elif isinstance(arg, Initialize):
|
|
621
666
|
context.console.print("Calling initial info tool")
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
667
|
+
if arg.type == "user_asked_mode_change" or arg.type == "reset_shell":
|
|
668
|
+
workspace_path = (
|
|
669
|
+
arg.any_workspace_path
|
|
670
|
+
if os.path.isdir(arg.any_workspace_path)
|
|
671
|
+
else os.path.dirname(arg.any_workspace_path)
|
|
672
|
+
)
|
|
673
|
+
workspace_path = workspace_path if os.path.exists(workspace_path) else ""
|
|
674
|
+
output = (
|
|
675
|
+
reset_wcgw(
|
|
676
|
+
context,
|
|
677
|
+
workspace_path,
|
|
678
|
+
arg.mode_name
|
|
679
|
+
if is_mode_change(arg.mode, context.bash_state)
|
|
680
|
+
else None,
|
|
681
|
+
arg.mode,
|
|
682
|
+
),
|
|
683
|
+
0.0,
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
output_, context = initialize(
|
|
687
|
+
arg.type,
|
|
688
|
+
context,
|
|
689
|
+
arg.any_workspace_path,
|
|
690
|
+
arg.initial_files_to_read,
|
|
691
|
+
arg.task_id_to_resume,
|
|
692
|
+
max_tokens,
|
|
693
|
+
arg.mode,
|
|
694
|
+
)
|
|
695
|
+
output = output_, 0.0
|
|
631
696
|
|
|
632
697
|
elif isinstance(arg, ContextSave):
|
|
633
698
|
context.console.print("Calling task memory tool")
|
|
634
699
|
relevant_files = []
|
|
635
700
|
warnings = ""
|
|
701
|
+
arg.project_root_path = os.path.expanduser(arg.project_root_path)
|
|
636
702
|
for fglob in arg.relevant_file_globs:
|
|
637
703
|
fglob = expand_user(fglob)
|
|
638
704
|
if not os.path.isabs(fglob) and arg.project_root_path:
|
|
@@ -642,11 +708,15 @@ def get_tool_output(
|
|
|
642
708
|
if not globs:
|
|
643
709
|
warnings += f"Warning: No files found for the glob: {fglob}\n"
|
|
644
710
|
relevant_files_data = read_files(relevant_files[:10_000], None, context)
|
|
645
|
-
|
|
711
|
+
save_path = save_memory(arg, relevant_files_data, context.bash_state.serialize())
|
|
646
712
|
if not relevant_files and arg.relevant_file_globs:
|
|
647
|
-
output_ = f'Error: No files found for the given globs. Context file successfully saved at "{
|
|
713
|
+
output_ = f'Error: No files found for the given globs. Context file successfully saved at "{save_path}", but please fix the error.'
|
|
648
714
|
elif warnings:
|
|
649
|
-
output_ = warnings + "\nContext file successfully saved at " +
|
|
715
|
+
output_ = warnings + "\nContext file successfully saved at " + save_path
|
|
716
|
+
else:
|
|
717
|
+
output_ = save_path
|
|
718
|
+
# Try to open the saved file
|
|
719
|
+
try_open_file(save_path)
|
|
650
720
|
output = output_, 0.0
|
|
651
721
|
else:
|
|
652
722
|
raise ValueError(f"Unknown tool: {arg}")
|
wcgw/relay/serve.py
CHANGED
|
@@ -19,7 +19,6 @@ from ..types_ import (
|
|
|
19
19
|
FileEdit,
|
|
20
20
|
Initialize,
|
|
21
21
|
ReadFiles,
|
|
22
|
-
ResetWcgw,
|
|
23
22
|
WriteIfEmpty,
|
|
24
23
|
)
|
|
25
24
|
|
|
@@ -28,7 +27,6 @@ class Mdata(BaseModel):
|
|
|
28
27
|
data: (
|
|
29
28
|
BashCommand
|
|
30
29
|
| WriteIfEmpty
|
|
31
|
-
| ResetWcgw
|
|
32
30
|
| FileEdit
|
|
33
31
|
| ReadFiles
|
|
34
32
|
| Initialize
|
|
@@ -160,35 +158,6 @@ async def file_edit_find_replace(
|
|
|
160
158
|
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
161
159
|
|
|
162
160
|
|
|
163
|
-
class ResetWcgwWithUUID(ResetWcgw):
|
|
164
|
-
user_id: UUID
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@app.post("/v1/reset_wcgw")
|
|
168
|
-
async def reset_wcgw(reset_wcgw: ResetWcgwWithUUID) -> str:
|
|
169
|
-
user_id = reset_wcgw.user_id
|
|
170
|
-
if user_id not in clients:
|
|
171
|
-
return "Failure: id not found, ask the user to check it."
|
|
172
|
-
|
|
173
|
-
results: Optional[str] = None
|
|
174
|
-
|
|
175
|
-
def put_results(result: str) -> None:
|
|
176
|
-
nonlocal results
|
|
177
|
-
results = result
|
|
178
|
-
|
|
179
|
-
gpts[user_id] = put_results
|
|
180
|
-
|
|
181
|
-
await clients[user_id](Mdata(data=reset_wcgw, user_id=user_id))
|
|
182
|
-
|
|
183
|
-
start_time = time.time()
|
|
184
|
-
while time.time() - start_time < 30:
|
|
185
|
-
if results is not None:
|
|
186
|
-
return results
|
|
187
|
-
await asyncio.sleep(0.1)
|
|
188
|
-
|
|
189
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
190
|
-
|
|
191
|
-
|
|
192
161
|
class CommandWithUUID(BashCommand):
|
|
193
162
|
user_id: UUID
|
|
194
163
|
|
wcgw/types_.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from enum import Enum
|
|
3
2
|
from typing import Any, Literal, Optional, Protocol, Sequence, Union
|
|
4
3
|
|
|
5
4
|
from pydantic import BaseModel as PydanticBaseModel
|
|
@@ -13,16 +12,23 @@ class NoExtraArgs(PydanticBaseModel):
|
|
|
13
12
|
BaseModel = NoExtraArgs
|
|
14
13
|
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
wcgw = "wcgw"
|
|
18
|
-
architect = "architect"
|
|
19
|
-
code_writer = "code_writer"
|
|
15
|
+
Modes = Literal["wcgw", "architect", "code_writer"]
|
|
20
16
|
|
|
21
17
|
|
|
22
18
|
class CodeWriterMode(BaseModel):
|
|
23
19
|
allowed_globs: Literal["all"] | list[str]
|
|
24
20
|
allowed_commands: Literal["all"] | list[str]
|
|
25
21
|
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
# Patch frequently wrong output trading off accuracy
|
|
24
|
+
# in rare case there's a file named 'all' or a command named 'all'
|
|
25
|
+
if len(self.allowed_commands) == 1:
|
|
26
|
+
if self.allowed_commands[0] == "all":
|
|
27
|
+
self.allowed_commands = "all"
|
|
28
|
+
if len(self.allowed_globs) == 1:
|
|
29
|
+
if self.allowed_globs[0] == "all":
|
|
30
|
+
self.allowed_globs = "all"
|
|
31
|
+
|
|
26
32
|
def update_relative_globs(self, workspace_root: str) -> None:
|
|
27
33
|
"""Update globs if they're relative paths"""
|
|
28
34
|
if self.allowed_globs != "all":
|
|
@@ -36,6 +42,12 @@ ModesConfig = Union[Literal["wcgw", "architect"], CodeWriterMode]
|
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
class Initialize(BaseModel):
|
|
45
|
+
type: Literal[
|
|
46
|
+
"first_call",
|
|
47
|
+
"user_asked_mode_change",
|
|
48
|
+
"reset_shell",
|
|
49
|
+
"user_asked_change_workspace",
|
|
50
|
+
]
|
|
39
51
|
any_workspace_path: str
|
|
40
52
|
initial_files_to_read: list[str]
|
|
41
53
|
task_id_to_resume: str
|
|
@@ -44,9 +56,9 @@ class Initialize(BaseModel):
|
|
|
44
56
|
|
|
45
57
|
def model_post_init(self, __context: Any) -> None:
|
|
46
58
|
if self.mode_name == "code_writer":
|
|
47
|
-
assert
|
|
48
|
-
|
|
49
|
-
)
|
|
59
|
+
assert (
|
|
60
|
+
self.code_writer_config is not None
|
|
61
|
+
), "code_writer_config can't be null when the mode is code_writer"
|
|
50
62
|
return super().model_post_init(__context)
|
|
51
63
|
|
|
52
64
|
@property
|
|
@@ -55,9 +67,9 @@ class Initialize(BaseModel):
|
|
|
55
67
|
return "wcgw"
|
|
56
68
|
if self.mode_name == "architect":
|
|
57
69
|
return "architect"
|
|
58
|
-
assert
|
|
59
|
-
|
|
60
|
-
)
|
|
70
|
+
assert (
|
|
71
|
+
self.code_writer_config is not None
|
|
72
|
+
), "code_writer_config can't be null when the mode is code_writer"
|
|
61
73
|
return self.code_writer_config
|
|
62
74
|
|
|
63
75
|
|
|
@@ -74,7 +86,7 @@ class SendText(BaseModel):
|
|
|
74
86
|
|
|
75
87
|
|
|
76
88
|
Specials = Literal[
|
|
77
|
-
"Enter", "Key-up", "Key-down", "Key-left", "Key-right", "Ctrl-c", "Ctrl-d"
|
|
89
|
+
"Enter", "Key-up", "Key-down", "Key-left", "Key-right", "Ctrl-c", "Ctrl-d"
|
|
78
90
|
]
|
|
79
91
|
|
|
80
92
|
|
|
@@ -104,13 +116,6 @@ class ReadFiles(BaseModel):
|
|
|
104
116
|
file_paths: list[str]
|
|
105
117
|
|
|
106
118
|
|
|
107
|
-
class ResetWcgw(BaseModel):
|
|
108
|
-
should_reset: Literal[True]
|
|
109
|
-
change_mode: Optional[Literal["wcgw", "architect", "code_writer"]]
|
|
110
|
-
code_writer_config: Optional[CodeWriterMode] = None
|
|
111
|
-
starting_directory: str
|
|
112
|
-
|
|
113
|
-
|
|
114
119
|
class FileEdit(BaseModel):
|
|
115
120
|
file_path: str
|
|
116
121
|
file_edit_using_search_replace_blocks: str
|
|
@@ -133,7 +138,6 @@ class Mdata(PydanticBaseModel):
|
|
|
133
138
|
data: (
|
|
134
139
|
BashCommand
|
|
135
140
|
| WriteIfEmpty
|
|
136
|
-
| ResetWcgw
|
|
137
141
|
| FileEdit
|
|
138
142
|
| str
|
|
139
143
|
| ReadFiles
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
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>
|
|
@@ -43,6 +43,8 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
43
43
|
|
|
44
44
|
## Updates
|
|
45
45
|
|
|
46
|
+
- [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
|
|
47
|
+
|
|
46
48
|
- [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
|
|
47
49
|
|
|
48
50
|
- [8 Jan 2025] Context saving tool for saving relevant file paths along with a description in a single file. Can be used as a task checkpoint or for knowledge transfer.
|
|
@@ -51,9 +53,6 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
51
53
|
|
|
52
54
|
- [9 Dec 2024] [Vscode extension to paste context on Claude app](https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw)
|
|
53
55
|
|
|
54
|
-
- [01 Dec 2024] Removed author hosted relay server for chatgpt.
|
|
55
|
-
|
|
56
|
-
- [26 Nov 2024] Introduced claude desktop support through mcp
|
|
57
56
|
|
|
58
57
|
## 🚀 Highlights
|
|
59
58
|
|
|
@@ -78,6 +77,7 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
78
77
|
- Ask it to run in 'code-writer' mode for code editing and project building. You can provide specific paths with wild card support to prevent other files getting edited.
|
|
79
78
|
- By default it runs in 'wcgw' mode that has no restrictions and full authorisation.
|
|
80
79
|
- More details in [Modes section](#modes)
|
|
80
|
+
- ⚡ **Runs in multiplex terminal** Run `screen -x` to attach to the terminal that the AI runs commands on. See history or interrupt process or interact with the same terminal that AI uses.
|
|
81
81
|
|
|
82
82
|
## Top use cases examples
|
|
83
83
|
|
|
@@ -100,7 +100,7 @@ First install `uv` using homebrew `brew install uv`
|
|
|
100
100
|
|
|
101
101
|
(**Important:** use homebrew to install uv. Otherwise make sure `uv` is present in a global location like /usr/bin/)
|
|
102
102
|
|
|
103
|
-
Then update `claude_desktop_config.json` (~/Library/Application Support/Claude/claude_desktop_config.json)
|
|
103
|
+
Then create or update `claude_desktop_config.json` (~/Library/Application Support/Claude/claude_desktop_config.json) with following json.
|
|
104
104
|
|
|
105
105
|
```json
|
|
106
106
|
{
|
|
@@ -127,6 +127,8 @@ _If there's an error in setting up_
|
|
|
127
127
|
|
|
128
128
|
- If there's an error like "uv ENOENT", make sure `uv` is installed. Then run 'which uv' in the terminal, and use its output in place of "uv" in the configuration.
|
|
129
129
|
- 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.
|
|
130
|
+
- Try removing ~/.cache/uv folder
|
|
131
|
+
- Try using `uv` version `0.6.0` for which this tool was tested.
|
|
130
132
|
- Debug the mcp server using `npx @modelcontextprotocol/inspector@0.1.7 uv tool run --from wcgw@latest --python 3.12 wcgw_mcp`
|
|
131
133
|
|
|
132
134
|
### Alternative configuration using smithery (npx required)
|
|
@@ -139,6 +141,9 @@ Then to configure wcgw for Claude Desktop automatically via [Smithery](https://s
|
|
|
139
141
|
npx -y @smithery/cli install wcgw --client claude
|
|
140
142
|
```
|
|
141
143
|
|
|
144
|
+
_If there's an error in setting up_
|
|
145
|
+
- Try removing ~/.cache/uv folder
|
|
146
|
+
|
|
142
147
|
### Usage
|
|
143
148
|
|
|
144
149
|
Wait for a few seconds. You should be able to see this icon if everything goes right.
|
|
@@ -168,6 +173,21 @@ There are three built-in modes. You may ask Claude to run in one of the modes, l
|
|
|
168
173
|
|
|
169
174
|
Note: in code-writer mode either all commands are allowed or none are allowed for now. If you give a list of allowed commands, Claude is instructed to run only those commands, but no actual check happens. (WIP)
|
|
170
175
|
|
|
176
|
+
#### Attach to the working terminal to investigate
|
|
177
|
+
If you've `screen` command installed, wcgw runs on a screen instance automatically. If you've started wcgw mcp server, you can list the screen sessions:
|
|
178
|
+
|
|
179
|
+
`screen -ls`
|
|
180
|
+
|
|
181
|
+
And note down the wcgw screen name which will be something like `93358.wcgw.235521` where the last number is in the hour-minute-second format.
|
|
182
|
+
|
|
183
|
+
You can then attach to the session using `screen -x 93358.wcgw.235521`
|
|
184
|
+
|
|
185
|
+
You may interrupt any running command safely.
|
|
186
|
+
|
|
187
|
+
You can interact with the terminal but beware that the AI might be running in parallel and it may conflict with what you're doing. It's recommended to keep your interactions to minimum.
|
|
188
|
+
|
|
189
|
+
You shouldn't exit the session using `exit `or Ctrl-d, instead you should use `ctrl+a+d` to safely detach without destroying the screen session.
|
|
190
|
+
|
|
171
191
|
### [Optional] Vs code extension
|
|
172
192
|
|
|
173
193
|
https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw
|
|
@@ -216,7 +236,6 @@ The server provides the following MCP tools:
|
|
|
216
236
|
- Parameters: `any_workspace_path` (string), `initial_files_to_read` (string[]), `mode_name` ("wcgw"|"architect"|"code_writer"), `task_id_to_resume` (string)
|
|
217
237
|
- `BashCommand`: Execute shell commands with timeout control
|
|
218
238
|
- Parameters: `command` (string), `wait_for_seconds` (int, optional)
|
|
219
|
-
- `BashInteraction`: Send keyboard input to running programs
|
|
220
239
|
- Parameters: `send_text` (string) or `send_specials` (["Enter"|"Key-up"|...]) or `send_ascii` (int[]), `wait_for_seconds` (int, optional)
|
|
221
240
|
|
|
222
241
|
**File Operations:**
|
|
@@ -234,7 +253,5 @@ The server provides the following MCP tools:
|
|
|
234
253
|
|
|
235
254
|
- `ContextSave`: Save project context and files for Knowledge Transfer or saving task checkpoints to be resumed later
|
|
236
255
|
- Parameters: `id` (string), `project_root_path` (string), `description` (string), `relevant_file_globs` (string[])
|
|
237
|
-
- `ResetShell`: Emergency reset for shell environment
|
|
238
|
-
- Parameters: `should_reset` (boolean)
|
|
239
256
|
|
|
240
257
|
All tools support absolute paths and include built-in protections against common errors. See the [MCP specification](https://modelcontextprotocol.io/) for detailed protocol information.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
wcgw/__init__.py,sha256=qUofQOAXCGcWr2u_B8U-MIMhhYaBUpUwNDcscvRmYfo,90
|
|
2
2
|
wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
wcgw/types_.py,sha256=
|
|
3
|
+
wcgw/types_.py,sha256=KFyfLFrctU9jr3zoQR3nlAMlP17hu91bM7ZeUTdTZ74,3624
|
|
4
4
|
wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
wcgw/client/common.py,sha256=OCH7Tx64jojz3M3iONUrGMadE07W21DiZs5sOxWX1Qc,1456
|
|
6
6
|
wcgw/client/diff-instructions.txt,sha256=tmJ9Fu9XdO_72lYXQQNY9RZyx91bjxrXJf9d_KBz57k,1611
|
|
7
7
|
wcgw/client/memory.py,sha256=8LdYsOhvCOoC1kfvDr85kNy07WnhPMvE6B2FRM2w85Y,2902
|
|
8
|
-
wcgw/client/modes.py,sha256=
|
|
9
|
-
wcgw/client/tool_prompts.py,sha256=
|
|
10
|
-
wcgw/client/tools.py,sha256=
|
|
11
|
-
wcgw/client/bash_state/bash_state.py,sha256=
|
|
8
|
+
wcgw/client/modes.py,sha256=FjIQOjT5oI7dk9VG0oRemN1A6EHBvHnuSQGKuRbguJI,10352
|
|
9
|
+
wcgw/client/tool_prompts.py,sha256=H36Sr3yH2YuHZTc08Y6rTUWlYWjKMFtIGP6vbAVNwZo,3988
|
|
10
|
+
wcgw/client/tools.py,sha256=hwKF6Wydiw-xqVDBJ2wZJ5BhZhtEQikM0G_YpolZ4zg,25398
|
|
11
|
+
wcgw/client/bash_state/bash_state.py,sha256=RoNR5qyskYPeLtaBkatHNPWhAAJI2AaPJ7Z3gAKvONU,26568
|
|
12
12
|
wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
|
|
13
13
|
wcgw/client/file_ops/diff_edit.py,sha256=sIwXSSkWYff_Dp3oHfefqtSuFqLrxbhKvzkCoLuLqDE,18679
|
|
14
14
|
wcgw/client/file_ops/search_replace.py,sha256=Napa7IWaYPGMNdttunKyRDkb90elZE7r23B_o_htRxo,5585
|
|
@@ -21,13 +21,13 @@ wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4
|
|
|
21
21
|
wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5rxTjoNnCos,1569052
|
|
22
22
|
wcgw/client/repo_ops/repo_context.py,sha256=5NqRxBY0K-SBFXJ0Ybt7llzYOBD8pRkTpruMMJHWxv4,4336
|
|
23
23
|
wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
|
|
24
|
-
wcgw/relay/serve.py,sha256=
|
|
24
|
+
wcgw/relay/serve.py,sha256=Vl7Nb68-F910LwHrTElfCNwajF37CfgObUIAdwhrRsI,7886
|
|
25
25
|
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
26
26
|
wcgw_cli/__init__.py,sha256=TNxXsTPgb52OhakIda9wTRh91cqoBqgQRx5TxjzQQFU,21
|
|
27
27
|
wcgw_cli/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
28
|
-
wcgw_cli/anthropic_client.py,sha256=
|
|
28
|
+
wcgw_cli/anthropic_client.py,sha256=gQc1opw-N5ecqcORbvvHGBW1Ac-Soe7c7APJD25HIfo,19887
|
|
29
29
|
wcgw_cli/cli.py,sha256=-7FBe_lahKyUOhf65iurTA1M1gXXXAiT0OVKQVcZKKo,948
|
|
30
|
-
wcgw_cli/openai_client.py,sha256=
|
|
30
|
+
wcgw_cli/openai_client.py,sha256=o4vfhcfGKqoWYWnn5YxNw4muKyDnFEiPmWpZpufDGMA,16021
|
|
31
31
|
wcgw_cli/openai_utils.py,sha256=xGOb3W5ALrIozV7oszfGYztpj0FnXdD7jAxm5lEIVKY,2439
|
|
32
32
|
mcp_wcgw/__init__.py,sha256=fKCgOdN7cn7gR3YGFaGyV5Goe8A2sEyllLcsRkN0i-g,2601
|
|
33
33
|
mcp_wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -51,8 +51,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
51
51
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
52
52
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
53
53
|
mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
|
|
54
|
-
wcgw-3.0.
|
|
55
|
-
wcgw-3.0.
|
|
56
|
-
wcgw-3.0.
|
|
57
|
-
wcgw-3.0.
|
|
58
|
-
wcgw-3.0.
|
|
54
|
+
wcgw-3.0.3.dist-info/METADATA,sha256=oG6kjnyYUCJzKiINctH5M0mlDo4dEwS88WN15WdafUg,14049
|
|
55
|
+
wcgw-3.0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
56
|
+
wcgw-3.0.3.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
|
|
57
|
+
wcgw-3.0.3.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
58
|
+
wcgw-3.0.3.dist-info/RECORD,,
|
wcgw_cli/anthropic_client.py
CHANGED
|
@@ -22,7 +22,7 @@ from anthropic.types import (
|
|
|
22
22
|
ToolUseBlockParam,
|
|
23
23
|
)
|
|
24
24
|
from dotenv import load_dotenv
|
|
25
|
-
from pydantic import BaseModel
|
|
25
|
+
from pydantic import BaseModel, ValidationError
|
|
26
26
|
from typer import Typer
|
|
27
27
|
|
|
28
28
|
from wcgw.client.bash_state.bash_state import BashState
|
|
@@ -218,6 +218,7 @@ def loop(
|
|
|
218
218
|
) as bash_state:
|
|
219
219
|
context = Context(bash_state, system_console)
|
|
220
220
|
system, context = initialize(
|
|
221
|
+
"first_call",
|
|
221
222
|
context,
|
|
222
223
|
os.getcwd(),
|
|
223
224
|
[],
|
|
@@ -379,14 +380,6 @@ def loop(
|
|
|
379
380
|
tool_input = str(tc["input"])
|
|
380
381
|
tool_id = str(tc["id"])
|
|
381
382
|
|
|
382
|
-
tool_parsed = parse_tool_by_name(
|
|
383
|
-
tool_name, json.loads(tool_input)
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
system_console.print(
|
|
387
|
-
f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
|
|
388
|
-
)
|
|
389
|
-
|
|
390
383
|
_histories.append(
|
|
391
384
|
{
|
|
392
385
|
"role": "assistant",
|
|
@@ -394,12 +387,35 @@ def loop(
|
|
|
394
387
|
ToolUseBlockParam(
|
|
395
388
|
id=tool_id,
|
|
396
389
|
name=tool_name,
|
|
397
|
-
input=
|
|
390
|
+
input=json.loads(tool_input),
|
|
398
391
|
type="tool_use",
|
|
399
392
|
)
|
|
400
393
|
],
|
|
401
394
|
}
|
|
402
395
|
)
|
|
396
|
+
try:
|
|
397
|
+
tool_parsed = parse_tool_by_name(
|
|
398
|
+
tool_name, json.loads(tool_input)
|
|
399
|
+
)
|
|
400
|
+
except ValidationError:
|
|
401
|
+
error_msg = f"Error parsing tool {tool_name}\n{traceback.format_exc()}"
|
|
402
|
+
system_console.log(
|
|
403
|
+
f"Error parsing tool {tool_name}"
|
|
404
|
+
)
|
|
405
|
+
tool_results.append(
|
|
406
|
+
ToolResultBlockParam(
|
|
407
|
+
type="tool_result",
|
|
408
|
+
tool_use_id=str(tc["id"]),
|
|
409
|
+
content=error_msg,
|
|
410
|
+
is_error=True,
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
system_console.print(
|
|
416
|
+
f"\n---------------------------------------\n# Assistant invoked tool: {tool_parsed}"
|
|
417
|
+
)
|
|
418
|
+
|
|
403
419
|
try:
|
|
404
420
|
output_or_dones, _ = get_tool_output(
|
|
405
421
|
context,
|
wcgw_cli/openai_client.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|