wcgw 3.0.1rc2__py3-none-any.whl → 3.0.2__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,
@@ -32,7 +30,8 @@ from ...types_ import (
32
30
  from ..encoder import EncoderDecoder
33
31
  from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
34
32
 
35
- PROMPT_CONST = "#" + "@wcgw@#"
33
+ PROMPT_CONST = "wcgw→" + " "
34
+ PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
36
35
  BASH_CLF_OUTPUT = Literal["repl", "pending"]
37
36
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
38
37
 
@@ -79,9 +78,7 @@ def get_tmpdir() -> str:
79
78
 
80
79
  def check_if_screen_command_available() -> bool:
81
80
  try:
82
- subprocess.run(
83
- ["screen", "-v"], capture_output=True, check=True, timeout=CONFIG.timeout
84
- )
81
+ subprocess.run(["screen", "-v"], capture_output=True, check=True, timeout=0.2)
85
82
  return True
86
83
  except (subprocess.CalledProcessError, FileNotFoundError):
87
84
  return False
@@ -100,7 +97,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
100
97
  capture_output=True,
101
98
  text=True,
102
99
  check=True,
103
- timeout=CONFIG.timeout,
100
+ timeout=0.2,
104
101
  )
105
102
  output = result.stdout
106
103
  except subprocess.CalledProcessError as e:
@@ -108,6 +105,9 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
108
105
  output = (e.stdout or "") + (e.stderr or "")
109
106
  except FileNotFoundError:
110
107
  return
108
+ except Exception as e:
109
+ console.log(f"{e}: exception while clearing running screens.")
110
+ return
111
111
 
112
112
  sessions_to_kill = []
113
113
 
@@ -131,8 +131,8 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
131
131
  check=True,
132
132
  timeout=CONFIG.timeout,
133
133
  )
134
- except (subprocess.CalledProcessError, FileNotFoundError):
135
- 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}")
136
136
 
137
137
 
138
138
  def start_shell(
@@ -146,23 +146,21 @@ def start_shell(
146
146
  **os.environ,
147
147
  "PS1": PROMPT_CONST,
148
148
  "TMPDIR": get_tmpdir(),
149
- "TERM": "vt100",
149
+ "TERM": "xterm-256color",
150
150
  }
151
151
  try:
152
152
  shell = pexpect.spawn(
153
153
  cmd,
154
154
  env=overrideenv, # type: ignore[arg-type]
155
- echo=False,
155
+ echo=True,
156
156
  encoding="utf-8",
157
157
  timeout=CONFIG.timeout,
158
158
  cwd=initial_dir,
159
159
  codec_errors="backslashreplace",
160
160
  dimensions=(500, 160),
161
161
  )
162
- shell.sendline(
163
- f"export PROMPT_COMMAND= PS1={PROMPT_CONST}"
164
- ) # Unset prompt command to avoid interfering
165
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
162
+ shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
163
+ shell.expect(PROMPT_CONST, timeout=0.2)
166
164
  except Exception as e:
167
165
  console.print(traceback.format_exc())
168
166
  console.log(f"Error starting shell: {e}. Retrying without rc ...")
@@ -170,13 +168,13 @@ def start_shell(
170
168
  shell = pexpect.spawn(
171
169
  "/bin/bash --noprofile --norc",
172
170
  env=overrideenv, # type: ignore[arg-type]
173
- echo=False,
171
+ echo=True,
174
172
  encoding="utf-8",
175
173
  timeout=CONFIG.timeout,
176
174
  codec_errors="backslashreplace",
177
175
  )
178
- shell.sendline(f"export PS1={PROMPT_CONST}")
179
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
176
+ shell.sendline(PROMPT_STATEMENT)
177
+ shell.expect(PROMPT_CONST, timeout=0.2)
180
178
 
181
179
  shellid = "wcgw." + time.strftime("%H%M%S")
182
180
  if over_screen:
@@ -184,20 +182,11 @@ def start_shell(
184
182
  raise ValueError("Screen command not available")
185
183
  # shellid is just hour, minute, second number
186
184
  shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
187
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
185
+ shell.expect(PROMPT_CONST, timeout=0.2)
188
186
 
189
187
  shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
190
188
  shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
191
189
 
192
- shell.sendline("stty -icanon -echo")
193
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
194
-
195
- shell.sendline("set +o pipefail")
196
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
197
-
198
- shell.sendline("export GIT_PAGER=cat PAGER=cat")
199
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
200
-
201
190
  return shell, shellid
202
191
 
203
192
 
@@ -229,31 +218,7 @@ P = ParamSpec("P")
229
218
  R = TypeVar("R")
230
219
 
231
220
 
232
- def requires_shell(
233
- func: Callable[Concatenate["BashState", "pexpect.spawn[str]", P], R],
234
- ) -> Callable[Concatenate["BashState", P], R]:
235
- def wrapper(self: "BashState", /, *args: P.args, **kwargs: P.kwargs) -> R:
236
- if not self._shell_loading.is_set():
237
- if not self._shell_loading.wait(timeout=CONFIG.timeout):
238
- raise RuntimeError("Shell initialization timeout")
239
-
240
- if self._shell_error:
241
- raise RuntimeError(f"Shell failed to initialize: {self._shell_error}.")
242
-
243
- if not self._shell:
244
- raise RuntimeError("Shell not initialized")
245
-
246
- return func(self, self._shell, *args, **kwargs)
247
-
248
- return wrapper
249
-
250
-
251
221
  class BashState:
252
- _shell: Optional["pexpect.spawn[str]"]
253
- _shell_id: Optional[str]
254
- _shell_lock: threading.Lock
255
- _shell_loading: threading.Event
256
- _shell_error: Optional[Exception]
257
222
  _use_screen: bool
258
223
 
259
224
  def __init__(
@@ -276,68 +241,41 @@ class BashState:
276
241
  self._write_if_empty_mode: WriteIfEmptyMode = (
277
242
  write_if_empty_mode or WriteIfEmptyMode("all")
278
243
  )
279
- self._mode = mode or Modes.wcgw
244
+ self._mode = mode or "wcgw"
280
245
  self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
281
- self._prompt = PROMPT_CONST
282
246
  self._bg_expect_thread: Optional[threading.Thread] = None
283
247
  self._bg_expect_thread_stop_event = threading.Event()
284
- self._shell = None
285
- self._shell_id = None
286
- self._shell_lock = threading.Lock()
287
- self._shell_loading = threading.Event()
288
- self._shell_error = None
289
248
  self._use_screen = use_screen
290
- self._start_shell_loading()
249
+ self._init_shell()
291
250
 
292
- def _start_shell_loading(self) -> None:
293
- def load_shell() -> None:
294
- try:
295
- with self._shell_lock:
296
- if self._shell is not None:
297
- return
298
- self._init_shell()
299
- except Exception as e:
300
- self._shell_error = e
301
- finally:
302
- self._shell_loading.set()
303
-
304
- threading.Thread(target=load_shell).start()
305
-
306
- @requires_shell
307
- def expect(
308
- self, shell: "pexpect.spawn[str]", pattern: Any, timeout: Optional[float] = -1
309
- ) -> int:
251
+ def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
310
252
  self.close_bg_expect_thread()
311
- return shell.expect(pattern, timeout)
253
+ output = self._shell.expect(pattern, timeout)
254
+ return output
312
255
 
313
- @requires_shell
314
- def send(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
315
- output = shell.send(s)
316
- self.run_bg_expect_thread()
256
+ def send(self, s: str | bytes) -> int:
257
+ self.close_bg_expect_thread()
258
+ output = self._shell.send(s)
317
259
  return output
318
260
 
319
- @requires_shell
320
- def sendline(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
321
- output = shell.sendline(s)
322
- self.run_bg_expect_thread()
261
+ def sendline(self, s: str | bytes) -> int:
262
+ self.close_bg_expect_thread()
263
+ output = self._shell.sendline(s)
323
264
  return output
324
265
 
325
266
  @property
326
- @requires_shell
327
- def linesep(self, shell: "pexpect.spawn[str]") -> Any:
328
- return shell.linesep
267
+ def linesep(self) -> Any:
268
+ return self._shell.linesep
329
269
 
330
- @requires_shell
331
- def sendintr(self, shell: "pexpect.spawn[str]") -> None:
332
- shell.sendintr()
270
+ def sendintr(self) -> None:
271
+ self.close_bg_expect_thread()
272
+ self._shell.sendintr()
333
273
 
334
274
  @property
335
- @requires_shell
336
- def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
337
- return shell.before
275
+ def before(self) -> Optional[str]:
276
+ return self._shell.before
338
277
 
339
- @requires_shell
340
- def run_bg_expect_thread(self, shell: "pexpect.spawn[str]") -> None:
278
+ def run_bg_expect_thread(self) -> None:
341
279
  """
342
280
  Run background expect thread for handling shell interactions.
343
281
  """
@@ -346,7 +284,7 @@ class BashState:
346
284
  while True:
347
285
  if self._bg_expect_thread_stop_event.is_set():
348
286
  break
349
- output = shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
287
+ output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
350
288
  if output == 0:
351
289
  break
352
290
 
@@ -367,13 +305,8 @@ class BashState:
367
305
 
368
306
  def cleanup(self) -> None:
369
307
  self.close_bg_expect_thread()
370
- with self._shell_lock:
371
- if self._shell:
372
- self._shell.close(True)
373
- if self._shell_id:
374
- cleanup_all_screens_with_name(self._shell_id, self.console)
375
- self._shell = None
376
- self._shell_id = None
308
+ self._shell.close(True)
309
+ cleanup_all_screens_with_name(self._shell_id, self.console)
377
310
 
378
311
  def __enter__(self) -> "BashState":
379
312
  return self
@@ -397,59 +330,41 @@ class BashState:
397
330
  def write_if_empty_mode(self) -> WriteIfEmptyMode:
398
331
  return self._write_if_empty_mode
399
332
 
400
- @requires_shell
401
- def ensure_env_and_bg_jobs(self, _: "pexpect.spawn[str]") -> Optional[int]:
402
- return self._ensure_env_and_bg_jobs()
403
-
404
- def _ensure_env_and_bg_jobs(self) -> Optional[int]:
405
- # Do not add @requires_shell decorator here, as it will cause deadlock
406
-
407
- self.close_bg_expect_thread()
408
- assert self._shell is not None, "Bad state, shell is not initialized"
409
- if self._prompt != PROMPT_CONST:
410
- return None
333
+ def ensure_env_and_bg_jobs(self) -> Optional[int]:
411
334
  quick_timeout = 0.2 if not self.over_screen else 1
412
335
  # First reset the prompt in case venv was sourced or other reasons.
413
- self._shell.sendline(f"export PS1={self._prompt}")
414
- self._shell.expect(self._prompt, timeout=quick_timeout)
336
+ self.sendline(PROMPT_STATEMENT)
337
+ self.expect(PROMPT_CONST, timeout=quick_timeout)
415
338
  # Reset echo also if it was enabled
416
- self._shell.sendline("stty -icanon -echo")
417
- self._shell.expect(self._prompt, timeout=quick_timeout)
418
- self._shell.sendline("set +o pipefail")
419
- self._shell.expect(self._prompt, timeout=quick_timeout)
420
- self._shell.sendline("export GIT_PAGER=cat PAGER=cat")
421
- self._shell.expect(self._prompt, timeout=quick_timeout)
422
- self._shell.sendline("jobs | wc -l")
339
+ command = "jobs | wc -l"
340
+ self.sendline(command)
423
341
  before = ""
424
342
  counts = 0
425
343
  while not _is_int(before): # Consume all previous output
426
344
  try:
427
- self._shell.expect(self._prompt, timeout=quick_timeout)
345
+ self.expect(PROMPT_CONST, timeout=quick_timeout)
428
346
  except pexpect.TIMEOUT:
429
347
  self.console.print(f"Couldn't get exit code, before: {before}")
430
348
  raise
431
349
 
432
- before_val = self._shell.before
350
+ before_val = self.before
433
351
  if not isinstance(before_val, str):
434
352
  before_val = str(before_val)
435
353
  assert isinstance(before_val, str)
436
354
  before_lines = render_terminal_output(before_val)
437
- before = "\n".join(before_lines).strip()
355
+ before = "\n".join(before_lines).replace(command, "").strip()
438
356
  counts += 1
439
357
  if counts > 100:
440
358
  raise ValueError(
441
359
  "Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
442
360
  )
443
-
444
361
  try:
445
362
  return int(before)
446
363
  except ValueError:
447
364
  raise ValueError(f"Malformed output: {before}")
448
365
 
449
366
  def _init_shell(self) -> None:
450
- self._prompt = PROMPT_CONST
451
367
  self._state: Literal["repl"] | datetime.datetime = "repl"
452
- self._is_in_docker: Optional[str] = ""
453
368
  # Ensure self._cwd exists
454
369
  os.makedirs(self._cwd, exist_ok=True)
455
370
  try:
@@ -474,13 +389,17 @@ class BashState:
474
389
  self.over_screen = False
475
390
 
476
391
  self._pending_output = ""
477
- self._ensure_env_and_bg_jobs()
392
+ try:
393
+ self.ensure_env_and_bg_jobs()
394
+ except ValueError as e:
395
+ self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
396
+
397
+ self.run_bg_expect_thread()
478
398
 
479
399
  def set_pending(self, last_pending_output: str) -> None:
480
400
  if not isinstance(self._state, datetime.datetime):
481
401
  self._state = datetime.datetime.now()
482
402
  self._pending_output = last_pending_output
483
- self.run_bg_expect_thread()
484
403
 
485
404
  def set_repl(self) -> None:
486
405
  self._state = "repl"
@@ -492,26 +411,18 @@ class BashState:
492
411
  return "repl"
493
412
  return "pending"
494
413
 
495
- @property
496
- def is_in_docker(self) -> Optional[str]:
497
- return self._is_in_docker
498
-
499
- def set_in_docker(self, docker_image_id: str) -> None:
500
- self._is_in_docker = docker_image_id
501
-
502
414
  @property
503
415
  def cwd(self) -> str:
504
416
  return self._cwd
505
417
 
506
418
  @property
507
419
  def prompt(self) -> str:
508
- return self._prompt
420
+ return PROMPT_CONST
509
421
 
510
- @requires_shell
511
- def update_cwd(self, shell: "pexpect.spawn[str]") -> str:
512
- shell.sendline("pwd")
513
- shell.expect(self._prompt, timeout=0.2)
514
- 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
515
426
  if not isinstance(before_val, str):
516
427
  before_val = str(before_val)
517
428
  before_lines = render_terminal_output(before_val)
@@ -521,9 +432,7 @@ class BashState:
521
432
 
522
433
  def reset_shell(self) -> None:
523
434
  self.cleanup()
524
- self._shell_loading.clear()
525
- self._shell_error = None
526
- self._start_shell_loading()
435
+ self._init_shell()
527
436
 
528
437
  def serialize(self) -> dict[str, Any]:
529
438
  """Serialize BashState to a dictionary for saving"""
@@ -543,7 +452,7 @@ class BashState:
543
452
  BashCommandMode.deserialize(state["bash_command_mode"]),
544
453
  FileEditMode.deserialize(state["file_edit_mode"]),
545
454
  WriteIfEmptyMode.deserialize(state["write_if_empty_mode"]),
546
- Modes[str(state["mode"])],
455
+ state["mode"],
547
456
  state["whitelist_for_overwrite"],
548
457
  )
549
458
 
@@ -607,7 +516,7 @@ WAITING_INPUT_MESSAGE = """A command is already running. NOTE: You can't run mul
607
516
  1. Get its output using `send_ascii: [10] or send_specials: ["Enter"]`
608
517
  2. Use `send_ascii` or `send_specials` to give inputs to the running program, don't use `BashCommand` OR
609
518
  3. kill the previous program by sending ctrl+c first using `send_ascii` or `send_specials`
610
- 4. Send the process in background using `send_specials: ["Ctrl-z"]` followed by BashCommand: `bg`
519
+ 4. Interrupt and run the process in background by re-running it using screen
611
520
  """
612
521
 
613
522
 
@@ -637,9 +546,8 @@ def _incremental_text(text: str, last_pending_output: str) -> str:
637
546
  # text = render_terminal_output(text[-100_000:])
638
547
  text = text[-100_000:]
639
548
 
640
- last_pending_output_rendered_lines = render_terminal_output(last_pending_output)
641
- last_pending_output_rendered = "\n".join(last_pending_output_rendered_lines)
642
- last_rendered_lines = last_pending_output_rendered.split("\n")
549
+ last_rendered_lines = render_terminal_output(last_pending_output)
550
+ last_pending_output_rendered = "\n".join(last_rendered_lines)
643
551
  if not last_rendered_lines:
644
552
  return rstrip(render_terminal_output(text))
645
553
 
@@ -690,6 +598,27 @@ def execute_bash(
690
598
  bash_arg: BashCommand,
691
599
  max_tokens: Optional[int],
692
600
  timeout_s: Optional[float],
601
+ ) -> tuple[str, float]:
602
+ try:
603
+ output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
604
+
605
+ # Remove echo if it's a command
606
+ if isinstance(bash_arg.action, Command):
607
+ command = bash_arg.action.command.strip()
608
+ if output.startswith(command):
609
+ output = output[len(command) :]
610
+
611
+ finally:
612
+ bash_state.run_bg_expect_thread()
613
+ return output, cost
614
+
615
+
616
+ def _execute_bash(
617
+ bash_state: BashState,
618
+ enc: EncoderDecoder[int],
619
+ bash_arg: BashCommand,
620
+ max_tokens: Optional[int],
621
+ timeout_s: Optional[float],
693
622
  ) -> tuple[str, float]:
694
623
  try:
695
624
  is_interrupt = False
@@ -1,5 +1,4 @@
1
1
  import importlib
2
- import json
3
2
  import logging
4
3
  import os
5
4
  from typing import Any
@@ -9,7 +8,7 @@ import mcp_wcgw.types as types
9
8
  from mcp_wcgw.server import NotificationOptions, Server
10
9
  from mcp_wcgw.server.models import InitializationOptions
11
10
  from mcp_wcgw.types import Tool as ToolParam
12
- from pydantic import AnyUrl, ValidationError
11
+ from pydantic import AnyUrl
13
12
 
14
13
  from wcgw.client.modes import KTS
15
14
  from wcgw.client.tool_prompts import TOOL_PROMPTS
@@ -18,7 +17,13 @@ from ...types_ import (
18
17
  Initialize,
19
18
  )
20
19
  from ..bash_state.bash_state import CONFIG, BashState
21
- from ..tools import Context, default_enc, get_tool_output, which_tool_name
20
+ from ..tools import (
21
+ Context,
22
+ default_enc,
23
+ get_tool_output,
24
+ parse_tool_by_name,
25
+ which_tool_name,
26
+ )
22
27
 
23
28
  server = Server("wcgw")
24
29
 
@@ -104,20 +109,7 @@ async def handle_call_tool(
104
109
  raise ValueError("Missing arguments")
105
110
 
106
111
  tool_type = which_tool_name(name)
107
-
108
- try:
109
- tool_call = tool_type(**arguments)
110
- except ValidationError:
111
-
112
- def try_json(x: str) -> Any:
113
- if not isinstance(x, str):
114
- return x
115
- try:
116
- return json.loads(x)
117
- except json.JSONDecodeError:
118
- return x
119
-
120
- tool_call = tool_type(**{k: try_json(v) for k, v in arguments.items()})
112
+ tool_call = parse_tool_by_name(name, arguments)
121
113
 
122
114
  try:
123
115
  assert BASH_STATE
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`.
33
+ - If the user has mentioned a workspace or project root or any other file or folder use it to set `any_workspace_path`.
37
34
  - 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.
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(
@@ -79,11 +78,6 @@ 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",
wcgw/client/tools.py CHANGED
@@ -10,6 +10,7 @@ from os.path import expanduser
10
10
  from pathlib import Path
11
11
  from tempfile import NamedTemporaryFile
12
12
  from typing import (
13
+ Any,
13
14
  Callable,
14
15
  Literal,
15
16
  Optional,
@@ -21,7 +22,7 @@ from typing import (
21
22
  from openai.types.chat import (
22
23
  ChatCompletionMessageParam,
23
24
  )
24
- from pydantic import BaseModel, TypeAdapter
25
+ from pydantic import BaseModel, TypeAdapter, ValidationError
25
26
  from syntax_checker import check_syntax
26
27
 
27
28
  from wcgw.client.bash_state.bash_state import get_status
@@ -37,7 +38,6 @@ from ..types_ import (
37
38
  ModesConfig,
38
39
  ReadFiles,
39
40
  ReadImage,
40
- ResetWcgw,
41
41
  WriteIfEmpty,
42
42
  )
43
43
  from .bash_state.bash_state import (
@@ -67,13 +67,13 @@ INITIALIZED = False
67
67
 
68
68
  def get_mode_prompt(context: Context) -> str:
69
69
  mode_prompt = ""
70
- if context.bash_state.mode == Modes.code_writer:
70
+ if context.bash_state.mode == "code_writer":
71
71
  mode_prompt = code_writer_prompt(
72
72
  context.bash_state.file_edit_mode.allowed_globs,
73
73
  context.bash_state.write_if_empty_mode.allowed_globs,
74
74
  "all" if context.bash_state.bash_command_mode.allowed_commands else [],
75
75
  )
76
- elif context.bash_state.mode == Modes.architect:
76
+ elif context.bash_state.mode == "architect":
77
77
  mode_prompt = ARCHITECT_PROMPT
78
78
  else:
79
79
  mode_prompt = WCGW_PROMPT
@@ -82,6 +82,7 @@ def get_mode_prompt(context: Context) -> str:
82
82
 
83
83
 
84
84
  def initialize(
85
+ type: Literal["user_asked_change_workspace", "first_call"],
85
86
  context: Context,
86
87
  any_workspace_path: str,
87
88
  read_files_: list[str],
@@ -95,20 +96,26 @@ def initialize(
95
96
 
96
97
  memory = ""
97
98
  loaded_state = None
98
- if task_id_to_resume:
99
- try:
100
- project_root_path, task_mem, loaded_state = load_memory(
101
- task_id_to_resume,
102
- max_tokens,
103
- lambda x: default_enc.encoder(x),
104
- lambda x: default_enc.decoder(x),
105
- )
106
- memory = "Following is the retrieved task:\n" + task_mem
107
- if os.path.exists(project_root_path):
108
- any_workspace_path = project_root_path
109
99
 
110
- except Exception:
111
- memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
100
+ if type == "first_call":
101
+ if task_id_to_resume:
102
+ try:
103
+ project_root_path, task_mem, loaded_state = load_memory(
104
+ task_id_to_resume,
105
+ max_tokens,
106
+ lambda x: default_enc.encoder(x),
107
+ lambda x: default_enc.decoder(x),
108
+ )
109
+ memory = "Following is the retrieved task:\n" + task_mem
110
+ if os.path.exists(project_root_path):
111
+ any_workspace_path = project_root_path
112
+
113
+ except Exception:
114
+ memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
115
+ elif task_id_to_resume:
116
+ memory = (
117
+ "Warning: task can only be resumed in a new conversation. No task loaded."
118
+ )
112
119
 
113
120
  folder_to_start = None
114
121
  if any_workspace_path:
@@ -164,7 +171,9 @@ def initialize(
164
171
  context.console.print(traceback.format_exc())
165
172
  context.console.print("Error: couldn't load bash state")
166
173
  pass
174
+ mode_prompt = get_mode_prompt(context)
167
175
  else:
176
+ mode_changed = is_mode_change(mode, context.bash_state)
168
177
  state = modes_to_state(mode)
169
178
  context.bash_state.load_state(
170
179
  state[0],
@@ -174,6 +183,11 @@ def initialize(
174
183
  list(context.bash_state.whitelist_for_overwrite),
175
184
  str(folder_to_start) if folder_to_start else "",
176
185
  )
186
+ if type == "first_call" or mode_changed:
187
+ mode_prompt = get_mode_prompt(context)
188
+ else:
189
+ mode_prompt = ""
190
+
177
191
  del mode
178
192
 
179
193
  initial_files_context = ""
@@ -188,7 +202,7 @@ def initialize(
188
202
 
189
203
  uname_sysname = os.uname().sysname
190
204
  uname_machine = os.uname().machine
191
- mode_prompt = get_mode_prompt(context)
205
+
192
206
  output = f"""
193
207
  {mode_prompt}
194
208
 
@@ -211,20 +225,34 @@ Initialized in directory (also cwd): {context.bash_state.cwd}
211
225
  return output, context
212
226
 
213
227
 
214
- def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
215
- if reset_wcgw.change_mode:
216
- # Convert to the type expected by modes_to_state
217
- mode_config: ModesConfig
218
- if reset_wcgw.change_mode == "code_writer":
219
- if not reset_wcgw.code_writer_config:
220
- return "Error: code_writer_config is required when changing to code_writer mode"
221
- mode_config = reset_wcgw.code_writer_config
228
+ def is_mode_change(mode_config: ModesConfig, bash_state: BashState) -> bool:
229
+ allowed = modes_to_state(mode_config)
230
+ bash_allowed = (
231
+ bash_state.bash_command_mode,
232
+ bash_state.file_edit_mode,
233
+ bash_state.write_if_empty_mode,
234
+ bash_state.mode,
235
+ )
236
+ return allowed != bash_allowed
237
+
238
+
239
+ def reset_wcgw(
240
+ context: Context,
241
+ starting_directory: str,
242
+ mode_name: Optional[Modes],
243
+ change_mode: ModesConfig,
244
+ ) -> str:
245
+ global INITIALIZED
246
+ if mode_name:
247
+ # update modes if they're relative
248
+ if isinstance(change_mode, CodeWriterMode):
249
+ change_mode.update_relative_globs(starting_directory)
222
250
  else:
223
- mode_config = reset_wcgw.change_mode
251
+ assert isinstance(change_mode, str)
224
252
 
225
253
  # Get new state configuration
226
254
  bash_command_mode, file_edit_mode, write_if_empty_mode, mode = modes_to_state(
227
- mode_config
255
+ change_mode
228
256
  )
229
257
 
230
258
  # Reset shell with new mode
@@ -234,11 +262,12 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
234
262
  write_if_empty_mode,
235
263
  mode,
236
264
  list(context.bash_state.whitelist_for_overwrite),
237
- reset_wcgw.starting_directory,
265
+ starting_directory,
238
266
  )
239
267
  mode_prompt = get_mode_prompt(context)
268
+ INITIALIZED = True
240
269
  return (
241
- f"Reset successful with mode change to {reset_wcgw.change_mode}.\n"
270
+ f"Reset successful with mode change to {mode_name}.\n"
242
271
  + mode_prompt
243
272
  + "\n"
244
273
  + get_status(context.bash_state)
@@ -257,9 +286,8 @@ def reset_wcgw(context: Context, reset_wcgw: ResetWcgw) -> str:
257
286
  write_if_empty_mode,
258
287
  mode,
259
288
  list(context.bash_state.whitelist_for_overwrite),
260
- reset_wcgw.starting_directory,
289
+ starting_directory,
261
290
  )
262
- global INITIALIZED
263
291
  INITIALIZED = True
264
292
  return "Reset successful" + get_status(context.bash_state)
265
293
 
@@ -513,7 +541,6 @@ Syntax errors:
513
541
 
514
542
  TOOLS = (
515
543
  BashCommand
516
- | ResetWcgw
517
544
  | WriteIfEmpty
518
545
  | FileEdit
519
546
  | ReadImage
@@ -531,8 +558,6 @@ def which_tool(args: str) -> TOOLS:
531
558
  def which_tool_name(name: str) -> Type[TOOLS]:
532
559
  if name == "BashCommand":
533
560
  return BashCommand
534
- elif name == "ResetWcgw":
535
- return ResetWcgw
536
561
  elif name == "WriteIfEmpty":
537
562
  return WriteIfEmpty
538
563
  elif name == "FileEdit":
@@ -549,6 +574,23 @@ def which_tool_name(name: str) -> Type[TOOLS]:
549
574
  raise ValueError(f"Unknown tool name: {name}")
550
575
 
551
576
 
577
+ def parse_tool_by_name(name: str, arguments: dict[str, Any]) -> TOOLS:
578
+ tool_type = which_tool_name(name)
579
+ try:
580
+ return tool_type(**arguments)
581
+ except ValidationError:
582
+
583
+ def try_json(x: str) -> Any:
584
+ if not isinstance(x, str):
585
+ return x
586
+ try:
587
+ return json.loads(x)
588
+ except json.JSONDecodeError:
589
+ return x
590
+
591
+ return tool_type(**{k: try_json(v) for k, v in arguments.items()})
592
+
593
+
552
594
  TOOL_CALLS: list[TOOLS] = []
553
595
 
554
596
 
@@ -595,21 +637,37 @@ def get_tool_output(
595
637
  elif isinstance(arg, ReadFiles):
596
638
  context.console.print("Calling read file tool")
597
639
  output = read_files(arg.file_paths, max_tokens, context), 0.0
598
- elif isinstance(arg, ResetWcgw):
599
- context.console.print("Calling reset wcgw tool")
600
- output = reset_wcgw(context, arg), 0.0
601
-
602
640
  elif isinstance(arg, Initialize):
603
641
  context.console.print("Calling initial info tool")
604
- output_, context = initialize(
605
- context,
606
- arg.any_workspace_path,
607
- arg.initial_files_to_read,
608
- arg.task_id_to_resume,
609
- max_tokens,
610
- arg.mode,
611
- )
612
- output = output_, 0.0
642
+ if arg.type == "user_asked_mode_change" or arg.type == "reset_shell":
643
+ workspace_path = (
644
+ arg.any_workspace_path
645
+ if os.path.isdir(arg.any_workspace_path)
646
+ else os.path.dirname(arg.any_workspace_path)
647
+ )
648
+ workspace_path = workspace_path if os.path.exists(workspace_path) else ""
649
+ output = (
650
+ reset_wcgw(
651
+ context,
652
+ workspace_path,
653
+ arg.mode_name
654
+ if is_mode_change(arg.mode, context.bash_state)
655
+ else None,
656
+ arg.mode,
657
+ ),
658
+ 0.0,
659
+ )
660
+ else:
661
+ output_, context = initialize(
662
+ arg.type,
663
+ context,
664
+ arg.any_workspace_path,
665
+ arg.initial_files_to_read,
666
+ arg.task_id_to_resume,
667
+ max_tokens,
668
+ arg.mode,
669
+ )
670
+ output = output_, 0.0
613
671
 
614
672
  elif isinstance(arg, ContextSave):
615
673
  context.console.print("Calling task memory tool")
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.1rc2
3
+ Version: 3.0.2
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
 
@@ -100,7 +99,7 @@ First install `uv` using homebrew `brew install uv`
100
99
 
101
100
  (**Important:** use homebrew to install uv. Otherwise make sure `uv` is present in a global location like /usr/bin/)
102
101
 
103
- Then update `claude_desktop_config.json` (~/Library/Application Support/Claude/claude_desktop_config.json)
102
+ Then create or update `claude_desktop_config.json` (~/Library/Application Support/Claude/claude_desktop_config.json) with following json.
104
103
 
105
104
  ```json
106
105
  {
@@ -127,6 +126,8 @@ _If there's an error in setting up_
127
126
 
128
127
  - 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
128
  - 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.
129
+ - Try removing ~/.cache/uv folder
130
+ - Try using `uv` version `0.6.0` for which this tool was tested.
130
131
  - Debug the mcp server using `npx @modelcontextprotocol/inspector@0.1.7 uv tool run --from wcgw@latest --python 3.12 wcgw_mcp`
131
132
 
132
133
  ### Alternative configuration using smithery (npx required)
@@ -139,6 +140,9 @@ Then to configure wcgw for Claude Desktop automatically via [Smithery](https://s
139
140
  npx -y @smithery/cli install wcgw --client claude
140
141
  ```
141
142
 
143
+ _If there's an error in setting up_
144
+ - Try removing ~/.cache/uv folder
145
+
142
146
  ### Usage
143
147
 
144
148
  Wait for a few seconds. You should be able to see this icon if everything goes right.
@@ -168,6 +172,21 @@ There are three built-in modes. You may ask Claude to run in one of the modes, l
168
172
 
169
173
  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
174
 
175
+ #### Attach to the working terminal to investigate
176
+ 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:
177
+
178
+ `screen -ls`
179
+
180
+ 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.
181
+
182
+ You can then attach to the session using `screen -x 93358.wcgw.235521`
183
+
184
+ You may interrupt any running command safely.
185
+
186
+ 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.
187
+
188
+ 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.
189
+
171
190
  ### [Optional] Vs code extension
172
191
 
173
192
  https://marketplace.visualstudio.com/items?itemName=AmanRusia.wcgw
@@ -216,7 +235,6 @@ The server provides the following MCP tools:
216
235
  - Parameters: `any_workspace_path` (string), `initial_files_to_read` (string[]), `mode_name` ("wcgw"|"architect"|"code_writer"), `task_id_to_resume` (string)
217
236
  - `BashCommand`: Execute shell commands with timeout control
218
237
  - Parameters: `command` (string), `wait_for_seconds` (int, optional)
219
- - `BashInteraction`: Send keyboard input to running programs
220
238
  - Parameters: `send_text` (string) or `send_specials` (["Enter"|"Key-up"|...]) or `send_ascii` (int[]), `wait_for_seconds` (int, optional)
221
239
 
222
240
  **File Operations:**
@@ -234,7 +252,5 @@ The server provides the following MCP tools:
234
252
 
235
253
  - `ContextSave`: Save project context and files for Knowledge Transfer or saving task checkpoints to be resumed later
236
254
  - 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
255
 
240
256
  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,33 +1,33 @@
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=eXAGPKm7ViG2OgF1m_yLTPehtmkhQFWDoiLEkV7fOMQ,22660
11
- wcgw/client/bash_state/bash_state.py,sha256=dNCx2KRQSQHYihTsfIqtQNsd8j27CoSMH4I026Cz8pU,29308
8
+ wcgw/client/modes.py,sha256=FjIQOjT5oI7dk9VG0oRemN1A6EHBvHnuSQGKuRbguJI,10352
9
+ wcgw/client/tool_prompts.py,sha256=RzLO3XGMg2VJfURG1BiQzZ6IdDWXfH2syqM1N3w0770,3980
10
+ wcgw/client/tools.py,sha256=UtVH_Q7E7A6ertdY9yPe5ukwSO2zg1Ojlp3z6MqmURI,24402
11
+ wcgw/client/bash_state/bash_state.py,sha256=DND9vNIfFee-Djif3YZhIsnaXamI3TzXE-AhISnICZY,26476
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
15
15
  wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
16
16
  wcgw/client/mcp_server/__init__.py,sha256=mm7xhBIPwJpRT3u-Qsj4cKVMpVyucJoKRlbMP_gRRB0,343
17
- wcgw/client/mcp_server/server.py,sha256=aF8SLaCsPisQsQ0fOUOpTfP2S4EGEugaLupjCyhHA8o,5371
17
+ wcgw/client/mcp_server/server.py,sha256=ayK6qbzCveoQW7RO80m10cAIS3m-hvxzd15XhjiyxmE,5055
18
18
  wcgw/client/repo_ops/display_tree.py,sha256=E5q9mrGBb57NyvudSmRIG-fj4FUqupbzjmARpX8X0XY,4166
19
19
  wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
20
20
  wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
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=hpeqfLxYgfJpLgYmCRvE9r1Iu0ubGiBqmgEFzwKIvCM,18966
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.1rc2.dist-info/METADATA,sha256=J7O32iQuFetYvpv1pNkdHMsHbeV7o-3HYd6QNUzMszk,13005
55
- wcgw-3.0.1rc2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
- wcgw-3.0.1rc2.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
57
- wcgw-3.0.1rc2.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
58
- wcgw-3.0.1rc2.dist-info/RECORD,,
54
+ wcgw-3.0.2.dist-info/METADATA,sha256=VNXS8-pCPvOPt_Xdgq4UIxp4aeZ69V-7DbIe0WGT7Eo,13858
55
+ wcgw-3.0.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
+ wcgw-3.0.2.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
57
+ wcgw-3.0.2.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
58
+ wcgw-3.0.2.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
@@ -35,7 +35,7 @@ from wcgw.client.tools import (
35
35
  default_enc,
36
36
  get_tool_output,
37
37
  initialize,
38
- which_tool_name,
38
+ parse_tool_by_name,
39
39
  )
40
40
 
41
41
 
@@ -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 = which_tool_name(
383
- tool_name
384
- ).model_validate_json(tool_input)
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