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.

@@ -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 (subprocess.CalledProcessError, FileNotFoundError):
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 Modes.wcgw
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._start_shell_loading()
249
+ self._init_shell()
280
250
 
281
- def _start_shell_loading(self) -> None:
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 = shell.expect(pattern, timeout)
253
+ output = self._shell.expect(pattern, timeout)
302
254
  return output
303
255
 
304
- @requires_shell
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 = shell.send(s)
258
+ output = self._shell.send(s)
308
259
  return output
309
260
 
310
- @requires_shell
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 = shell.sendline(s)
263
+ output = self._shell.sendline(s)
314
264
  return output
315
265
 
316
266
  @property
317
- @requires_shell
318
- def linesep(self, shell: "pexpect.spawn[str]") -> Any:
319
- return shell.linesep
267
+ def linesep(self) -> Any:
268
+ return self._shell.linesep
320
269
 
321
- @requires_shell
322
- def sendintr(self, shell: "pexpect.spawn[str]") -> None:
270
+ def sendintr(self) -> None:
323
271
  self.close_bg_expect_thread()
324
- shell.sendintr()
272
+ self._shell.sendintr()
325
273
 
326
274
  @property
327
- @requires_shell
328
- def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
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 and self._bg_expect_thread.is_alive():
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
- with self._shell_lock:
367
- if self._shell:
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
- @requires_shell
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._shell.sendline(PROMPT_STATEMENT)
407
- self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
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._shell.sendline(command)
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._shell.expect(PROMPT_CONST, timeout=quick_timeout)
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._shell.before
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._ensure_env_and_bg_jobs()
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
- @requires_shell
499
- def update_cwd(self, shell: "pexpect.spawn[str]") -> str:
500
- shell.sendline("pwd")
501
- shell.expect(PROMPT_CONST, timeout=0.2)
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._shell_loading.clear()
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
- Modes[str(state["mode"])],
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. Send the process in background using `send_specials: ["Ctrl-z"]` followed by BashCommand: `bg`
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 or Ctrl-z or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
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 Ctrl-z or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
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 Ctrl-z or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
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
- Your response should be in self-critique and brainstorm style.
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
- Modes.wcgw: ModeImpl(
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
- Modes.architect: ModeImpl(
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
- Modes.code_writer: ModeImpl(
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[Modes[mode]] # converts str to Modes enum
176
- mode_name = Modes[mode]
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[Modes.code_writer]
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 = Modes.code_writer
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 = {Modes.wcgw: WCGW_KT, Modes.architect: ARCHITECT_KT, Modes.code_writer: WCGW_KT}
237
+ KTS = {"wcgw": WCGW_KT, "architect": ARCHITECT_KT, "code_writer": WCGW_KT}
@@ -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, use it to set `any_workspace_path`.
36
- - If the user has mentioned a folder or file with unclear project root, use the file or folder as `any_workspace_path`.
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
- - Call `ResetWcgw` if you want to change the mode later.
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 file paths only
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 file path only.
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 file path only.
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 == Modes.code_writer:
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 == Modes.architect:
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
- except Exception:
112
- memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
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
- mode_prompt = get_mode_prompt(context)
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 reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
216
- if reset_wcgw.change_mode:
217
- # Convert to the type expected by modes_to_state
218
- mode_config: ModesConfig
219
- if reset_wcgw.change_mode == "code_writer":
220
- if not reset_wcgw.code_writer_config:
221
- return "Error: code_writer_config is required when changing to code_writer mode"
222
- mode_config = reset_wcgw.code_writer_config
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
- mode_config = reset_wcgw.change_mode
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
- mode_config
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
- reset_wcgw.starting_directory,
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 {reset_wcgw.change_mode}.\n"
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
- reset_wcgw.starting_directory,
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
- output_, context = initialize(
623
- context,
624
- arg.any_workspace_path,
625
- arg.initial_files_to_read,
626
- arg.task_id_to_resume,
627
- max_tokens,
628
- arg.mode,
629
- )
630
- output = output_, 0.0
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
- output_ = save_memory(arg, relevant_files_data, context.bash_state.serialize())
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 "{output_}", but please fix the error.'
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 " + output_
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
- class Modes(str, Enum):
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 self.code_writer_config is not None, (
48
- "code_writer_config can't be null when the mode is code_writer"
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 self.code_writer_config is not None, (
59
- "code_writer_config can't be null when the mode is code_writer"
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", "Ctrl-z"
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.1rc3
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=_r4H9Kebu22nSxD8wvtGhAOGlJheshLBggI2IE9F7tY,3355
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=dBkiMNQevTFNkhplrDsDuSIyeU-wLmAIfAa9Dqowvk8,10387
9
- wcgw/client/tool_prompts.py,sha256=j82aYnlr1pcinsOYitSrlyYz4-9K3KcqnAfGqot21NQ,4158
10
- wcgw/client/tools.py,sha256=caj10NHeK5tZ5Gi0L_oXzPcKBycgXeNaLEbXAzYTpqo,23170
11
- wcgw/client/bash_state/bash_state.py,sha256=2cxzYN6x-vEJ4oasrCgoRyVQKpARKONcaNDLfLPDTG8,29476
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=Ofq6PjW3zVVA2-9MVviGRiUESTD3sXb-482Q4RV13q8,8664
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=2QLFBLbMeQuixF7Pz9j_hINHTG1CF9IYQZeri7zFuF0,18964
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=oMFAaOkvXQtOY7choylVRJfaF2SnWvRc02ygQhlhVqY,15995
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.1rc3.dist-info/METADATA,sha256=ymiydLYH56IAV_8FKq98BrGf-tt7Vwumtc8nUBvzs50,13005
55
- wcgw-3.0.1rc3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
- wcgw-3.0.1rc3.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
57
- wcgw-3.0.1rc3.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
58
- wcgw-3.0.1rc3.dist-info/RECORD,,
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,,
@@ -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=tool_parsed.model_dump(),
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
@@ -182,6 +182,7 @@ def loop(
182
182
  ) as bash_state:
183
183
  context = Context(bash_state, system_console)
184
184
  system, context = initialize(
185
+ "first_call",
185
186
  context,
186
187
  os.getcwd(),
187
188
  [],
File without changes