wcgw 3.0.1rc1__tar.gz → 3.0.1rc3__tar.gz

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.

Files changed (124) hide show
  1. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/PKG-INFO +1 -1
  2. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/pyproject.toml +1 -1
  3. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/bash_state/bash_state.py +181 -82
  4. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/file_ops/diff_edit.py +52 -1
  5. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/mcp_server/server.py +10 -18
  6. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/tools.py +23 -3
  7. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/relay/client.py +1 -1
  8. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw_cli/anthropic_client.py +5 -6
  9. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw_cli/openai_client.py +1 -1
  10. wcgw-3.0.1rc3/tests/test_edit.py +450 -0
  11. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/tests/test_mcp_server.py +8 -22
  12. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/uv.lock +1 -1
  13. wcgw-3.0.1rc1/tests/test_edit.py +0 -269
  14. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.github/workflows/python-publish.yml +0 -0
  15. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.github/workflows/python-tests.yml +0 -0
  16. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.github/workflows/python-types.yml +0 -0
  17. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.gitignore +0 -0
  18. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.gitmodules +0 -0
  19. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.python-version +0 -0
  20. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/.vscode/settings.json +0 -0
  21. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/Dockerfile +0 -0
  22. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/LICENSE +0 -0
  23. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/README.md +0 -0
  24. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/gpt_action_json_schema.json +0 -0
  25. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/gpt_instructions.txt +0 -0
  26. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/openai.md +0 -0
  27. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.git +0 -0
  28. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  29. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  30. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
  31. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
  32. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
  33. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
  34. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.gitignore +0 -0
  35. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/.python-version +0 -0
  36. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
  37. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
  38. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/LICENSE +0 -0
  39. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/README.md +0 -0
  40. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/RELEASE.md +0 -0
  41. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/SECURITY.md +0 -0
  42. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/README.md +0 -0
  43. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
  44. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
  45. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
  46. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
  47. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
  48. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
  49. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
  50. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
  51. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
  52. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
  53. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
  54. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
  55. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
  56. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
  57. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
  58. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
  59. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
  60. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
  61. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/pyproject.toml +0 -0
  62. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
  63. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
  64. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
  65. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
  66. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
  67. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
  68. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
  69. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
  70. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
  71. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
  72. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
  73. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
  74. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
  75. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
  76. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
  77. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
  78. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
  79. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
  80. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
  81. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
  82. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
  83. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
  84. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
  85. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
  86. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
  87. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
  88. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
  89. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
  90. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
  91. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
  92. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
  93. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
  94. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/mcp_wcgw_fork/uv.lock +0 -0
  95. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/__init__.py +0 -0
  96. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/__init__.py +0 -0
  97. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/common.py +0 -0
  98. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/diff-instructions.txt +0 -0
  99. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/encoder/__init__.py +0 -0
  100. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/file_ops/search_replace.py +0 -0
  101. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/mcp_server/Readme.md +0 -0
  102. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/mcp_server/__init__.py +0 -0
  103. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/memory.py +0 -0
  104. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/modes.py +0 -0
  105. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/repo_ops/display_tree.py +0 -0
  106. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/repo_ops/path_prob.py +0 -0
  107. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
  108. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
  109. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/repo_ops/repo_context.py +0 -0
  110. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/client/tool_prompts.py +0 -0
  111. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/py.typed +0 -0
  112. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/relay/serve.py +0 -0
  113. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/relay/static/privacy.txt +0 -0
  114. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw/types_.py +0 -0
  115. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw_cli/__init__.py +0 -0
  116. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw_cli/__main__.py +0 -0
  117. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw_cli/cli.py +0 -0
  118. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/src/wcgw_cli/openai_utils.py +0 -0
  119. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/static/claude-ss.jpg +0 -0
  120. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/static/computer-use.jpg +0 -0
  121. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/static/example.jpg +0 -0
  122. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/static/rocket-icon.png +0 -0
  123. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/static/ss1.png +0 -0
  124. {wcgw-3.0.1rc1 → wcgw-3.0.1rc3}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 3.0.1rc1
3
+ Version: 3.0.1rc3
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>
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }]
3
3
  name = "wcgw"
4
- version = "3.0.1rc1"
4
+ version = "3.0.1rc3"
5
5
  description = "Shell and coding agent on claude and chatgpt"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -6,7 +6,15 @@ import threading
6
6
  import time
7
7
  import traceback
8
8
  from dataclasses import dataclass
9
- from typing import Any, Literal, Optional
9
+ from typing import (
10
+ Any,
11
+ Callable,
12
+ Concatenate,
13
+ Literal,
14
+ Optional,
15
+ ParamSpec,
16
+ TypeVar,
17
+ )
10
18
 
11
19
  import pexpect
12
20
  import pyte
@@ -24,7 +32,8 @@ from ...types_ import (
24
32
  from ..encoder import EncoderDecoder
25
33
  from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
26
34
 
27
- PROMPT_CONST = "#" + "@wcgw@#"
35
+ PROMPT_CONST = "wcgw→" + " "
36
+ PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
28
37
  BASH_CLF_OUTPUT = Literal["repl", "pending"]
29
38
  os.environ["TOKENIZERS_PARALLELISM"] = "false"
30
39
 
@@ -63,7 +72,7 @@ def get_tmpdir() -> str:
63
72
  timeout=CONFIG.timeout,
64
73
  ).strip()
65
74
  return result
66
- except subprocess.CalledProcessError:
75
+ except (subprocess.CalledProcessError, FileNotFoundError):
67
76
  return "//tmp"
68
77
  except Exception:
69
78
  return ""
@@ -71,9 +80,7 @@ def get_tmpdir() -> str:
71
80
 
72
81
  def check_if_screen_command_available() -> bool:
73
82
  try:
74
- subprocess.run(
75
- ["screen", "-v"], capture_output=True, check=True, timeout=CONFIG.timeout
76
- )
83
+ subprocess.run(["screen", "-v"], capture_output=True, check=True, timeout=0.2)
77
84
  return True
78
85
  except (subprocess.CalledProcessError, FileNotFoundError):
79
86
  return False
@@ -92,12 +99,14 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
92
99
  capture_output=True,
93
100
  text=True,
94
101
  check=True,
95
- timeout=CONFIG.timeout,
102
+ timeout=0.2,
96
103
  )
97
104
  output = result.stdout
98
105
  except subprocess.CalledProcessError as e:
99
106
  # When no screens exist, screen may return a non-zero exit code.
100
107
  output = (e.stdout or "") + (e.stderr or "")
108
+ except FileNotFoundError:
109
+ return
101
110
 
102
111
  sessions_to_kill = []
103
112
 
@@ -121,7 +130,7 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
121
130
  check=True,
122
131
  timeout=CONFIG.timeout,
123
132
  )
124
- except subprocess.CalledProcessError:
133
+ except (subprocess.CalledProcessError, FileNotFoundError):
125
134
  console.log(f"Failed to kill screen session: {session}")
126
135
 
127
136
 
@@ -136,23 +145,21 @@ def start_shell(
136
145
  **os.environ,
137
146
  "PS1": PROMPT_CONST,
138
147
  "TMPDIR": get_tmpdir(),
139
- "TERM": "vt100",
148
+ "TERM": "xterm-256color",
140
149
  }
141
150
  try:
142
151
  shell = pexpect.spawn(
143
152
  cmd,
144
153
  env=overrideenv, # type: ignore[arg-type]
145
- echo=False,
154
+ echo=True,
146
155
  encoding="utf-8",
147
156
  timeout=CONFIG.timeout,
148
157
  cwd=initial_dir,
149
158
  codec_errors="backslashreplace",
150
159
  dimensions=(500, 160),
151
160
  )
152
- shell.sendline(
153
- f"export PROMPT_COMMAND= PS1={PROMPT_CONST}"
154
- ) # Unset prompt command to avoid interfering
155
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
161
+ shell.sendline(PROMPT_STATEMENT) # Unset prompt command to avoid interfering
162
+ shell.expect(PROMPT_CONST, timeout=0.2)
156
163
  except Exception as e:
157
164
  console.print(traceback.format_exc())
158
165
  console.log(f"Error starting shell: {e}. Retrying without rc ...")
@@ -160,13 +167,13 @@ def start_shell(
160
167
  shell = pexpect.spawn(
161
168
  "/bin/bash --noprofile --norc",
162
169
  env=overrideenv, # type: ignore[arg-type]
163
- echo=False,
170
+ echo=True,
164
171
  encoding="utf-8",
165
172
  timeout=CONFIG.timeout,
166
173
  codec_errors="backslashreplace",
167
174
  )
168
- shell.sendline(f"export PS1={PROMPT_CONST}")
169
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
175
+ shell.sendline(PROMPT_STATEMENT)
176
+ shell.expect(PROMPT_CONST, timeout=0.2)
170
177
 
171
178
  shellid = "wcgw." + time.strftime("%H%M%S")
172
179
  if over_screen:
@@ -174,22 +181,11 @@ def start_shell(
174
181
  raise ValueError("Screen command not available")
175
182
  # shellid is just hour, minute, second number
176
183
  shell.sendline(f"trap 'screen -X -S {shellid} quit' EXIT")
177
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
184
+ shell.expect(PROMPT_CONST, timeout=0.2)
178
185
 
179
- shell.sendline(f"screen -q -s /bin/bash -S {shellid}")
186
+ shell.sendline(f"screen -q -S {shellid} /bin/bash --noprofile --norc")
180
187
  shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
181
188
 
182
- console.log(f"Entering screen session, name: {shellid}")
183
-
184
- shell.sendline("stty -icanon -echo")
185
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
186
-
187
- shell.sendline("set +o pipefail")
188
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
189
-
190
- shell.sendline("export GIT_PAGER=cat PAGER=cat")
191
- shell.expect(PROMPT_CONST, timeout=CONFIG.timeout)
192
-
193
189
  return shell, shellid
194
190
 
195
191
 
@@ -217,7 +213,39 @@ def render_terminal_output(text: str) -> list[str]:
217
213
  return lines
218
214
 
219
215
 
216
+ P = ParamSpec("P")
217
+ R = TypeVar("R")
218
+
219
+
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
+
220
241
  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
+ _use_screen: bool
248
+
221
249
  def __init__(
222
250
  self,
223
251
  console: Console,
@@ -240,35 +268,65 @@ class BashState:
240
268
  )
241
269
  self._mode = mode or Modes.wcgw
242
270
  self._whitelist_for_overwrite: set[str] = whitelist_for_overwrite or set()
243
- self._prompt = PROMPT_CONST
244
271
  self._bg_expect_thread: Optional[threading.Thread] = None
245
272
  self._bg_expect_thread_stop_event = threading.Event()
246
- self._init_shell(use_screen)
247
-
248
- def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
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
+ self._use_screen = use_screen
279
+ self._start_shell_loading()
280
+
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:
249
300
  self.close_bg_expect_thread()
250
- return self._shell.expect(pattern, timeout)
301
+ output = shell.expect(pattern, timeout)
302
+ return output
251
303
 
252
- def send(self, s: str | bytes) -> int:
253
- output = self._shell.send(s)
254
- self.run_bg_expect_thread()
304
+ @requires_shell
305
+ def send(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
306
+ self.close_bg_expect_thread()
307
+ output = shell.send(s)
255
308
  return output
256
309
 
257
- def sendline(self, s: str | bytes) -> int:
258
- output = self._shell.sendline(s)
259
- self.run_bg_expect_thread()
310
+ @requires_shell
311
+ def sendline(self, shell: "pexpect.spawn[str]", s: str | bytes) -> int:
312
+ self.close_bg_expect_thread()
313
+ output = shell.sendline(s)
260
314
  return output
261
315
 
262
316
  @property
263
- def linesep(self) -> Any:
264
- return self._shell.linesep
317
+ @requires_shell
318
+ def linesep(self, shell: "pexpect.spawn[str]") -> Any:
319
+ return shell.linesep
265
320
 
266
- def sendintr(self) -> None:
267
- self._shell.sendintr()
321
+ @requires_shell
322
+ def sendintr(self, shell: "pexpect.spawn[str]") -> None:
323
+ self.close_bg_expect_thread()
324
+ shell.sendintr()
268
325
 
269
326
  @property
270
- def before(self) -> Optional[str]:
271
- return self._shell.before
327
+ @requires_shell
328
+ def before(self, shell: "pexpect.spawn[str]") -> Optional[str]:
329
+ return shell.before
272
330
 
273
331
  def run_bg_expect_thread(self) -> None:
274
332
  """
@@ -279,11 +337,16 @@ class BashState:
279
337
  while True:
280
338
  if self._bg_expect_thread_stop_event.is_set():
281
339
  break
340
+ if self._shell is None:
341
+ time.sleep(0.1)
342
+ continue
282
343
  output = self._shell.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=0.1)
283
344
  if output == 0:
284
345
  break
285
346
 
286
- if self._bg_expect_thread:
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
287
350
  self.close_bg_expect_thread()
288
351
 
289
352
  self._bg_expect_thread = threading.Thread(
@@ -300,8 +363,13 @@ class BashState:
300
363
 
301
364
  def cleanup(self) -> None:
302
365
  self.close_bg_expect_thread()
303
- self._shell.close(True)
304
- cleanup_all_screens_with_name(self._shell_id, self.console)
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
305
373
 
306
374
  def __enter__(self) -> "BashState":
307
375
  return self
@@ -325,26 +393,26 @@ class BashState:
325
393
  def write_if_empty_mode(self) -> WriteIfEmptyMode:
326
394
  return self._write_if_empty_mode
327
395
 
328
- def ensure_env_and_bg_jobs(self) -> Optional[int]:
329
- if self._prompt != PROMPT_CONST:
330
- return None
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"
331
404
  quick_timeout = 0.2 if not self.over_screen else 1
332
405
  # First reset the prompt in case venv was sourced or other reasons.
333
- self.sendline(f"export PS1={self._prompt}")
334
- self.expect(self._prompt, timeout=quick_timeout)
406
+ self._shell.sendline(PROMPT_STATEMENT)
407
+ self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
335
408
  # Reset echo also if it was enabled
336
- self.sendline("stty -icanon -echo")
337
- self.expect(self._prompt, timeout=quick_timeout)
338
- self.sendline("set +o pipefail")
339
- self.expect(self._prompt, timeout=quick_timeout)
340
- self.sendline("export GIT_PAGER=cat PAGER=cat")
341
- self.expect(self._prompt, timeout=quick_timeout)
342
- self.sendline("jobs | wc -l")
409
+ command = "jobs | wc -l"
410
+ self._shell.sendline(command)
343
411
  before = ""
344
412
  counts = 0
345
413
  while not _is_int(before): # Consume all previous output
346
414
  try:
347
- self.expect(self._prompt, timeout=quick_timeout)
415
+ self._shell.expect(PROMPT_CONST, timeout=quick_timeout)
348
416
  except pexpect.TIMEOUT:
349
417
  self.console.print(f"Couldn't get exit code, before: {before}")
350
418
  raise
@@ -354,20 +422,18 @@ class BashState:
354
422
  before_val = str(before_val)
355
423
  assert isinstance(before_val, str)
356
424
  before_lines = render_terminal_output(before_val)
357
- before = "\n".join(before_lines).strip()
425
+ before = "\n".join(before_lines).replace(command, "").strip()
358
426
  counts += 1
359
427
  if counts > 100:
360
428
  raise ValueError(
361
429
  "Error in understanding shell output. This shouldn't happen, likely shell is in a bad state, please reset it"
362
430
  )
363
-
364
431
  try:
365
432
  return int(before)
366
433
  except ValueError:
367
434
  raise ValueError(f"Malformed output: {before}")
368
435
 
369
- def _init_shell(self, use_screen: bool) -> None:
370
- self._prompt = PROMPT_CONST
436
+ def _init_shell(self) -> None:
371
437
  self._state: Literal["repl"] | datetime.datetime = "repl"
372
438
  self._is_in_docker: Optional[str] = ""
373
439
  # Ensure self._cwd exists
@@ -377,9 +443,9 @@ class BashState:
377
443
  self._bash_command_mode.bash_mode == "restricted_mode",
378
444
  self._cwd,
379
445
  self.console,
380
- over_screen=use_screen,
446
+ over_screen=self._use_screen,
381
447
  )
382
- self.over_screen = use_screen
448
+ self.over_screen = self._use_screen
383
449
  except Exception as e:
384
450
  if not isinstance(e, ValueError):
385
451
  self.console.log(traceback.format_exc())
@@ -394,15 +460,15 @@ class BashState:
394
460
  self.over_screen = False
395
461
 
396
462
  self._pending_output = ""
397
-
398
- # Get exit info to ensure shell is ready
399
- self.ensure_env_and_bg_jobs()
463
+ try:
464
+ self._ensure_env_and_bg_jobs()
465
+ except ValueError as e:
466
+ self.console.log("Error while running _ensure_env_and_bg_jobs" + str(e))
400
467
 
401
468
  def set_pending(self, last_pending_output: str) -> None:
402
469
  if not isinstance(self._state, datetime.datetime):
403
470
  self._state = datetime.datetime.now()
404
471
  self._pending_output = last_pending_output
405
- self.run_bg_expect_thread()
406
472
 
407
473
  def set_repl(self) -> None:
408
474
  self._state = "repl"
@@ -427,12 +493,13 @@ class BashState:
427
493
 
428
494
  @property
429
495
  def prompt(self) -> str:
430
- return self._prompt
496
+ return PROMPT_CONST
431
497
 
432
- def update_cwd(self) -> str:
433
- self.sendline("pwd")
434
- self.expect(self._prompt, timeout=0.2)
435
- before_val = self._shell.before
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
436
503
  if not isinstance(before_val, str):
437
504
  before_val = str(before_val)
438
505
  before_lines = render_terminal_output(before_val)
@@ -442,7 +509,9 @@ class BashState:
442
509
 
443
510
  def reset_shell(self) -> None:
444
511
  self.cleanup()
445
- self._init_shell(True)
512
+ self._shell_loading.clear()
513
+ self._shell_error = None
514
+ self._start_shell_loading()
446
515
 
447
516
  def serialize(self) -> dict[str, Any]:
448
517
  """Serialize BashState to a dictionary for saving"""
@@ -476,6 +545,16 @@ class BashState:
476
545
  cwd: str,
477
546
  ) -> None:
478
547
  """Create a new BashState instance from a serialized state dictionary"""
548
+ if (
549
+ self._bash_command_mode == bash_command_mode
550
+ and ((self._cwd == cwd) or not cwd)
551
+ and (self._file_edit_mode == file_edit_mode)
552
+ and (self._write_if_empty_mode == write_if_empty_mode)
553
+ and (self._mode == mode)
554
+ and (self._whitelist_for_overwrite == set(whitelist_for_overwrite))
555
+ ):
556
+ # No need to reset shell if the state is the same
557
+ return
479
558
  self._bash_command_mode = bash_command_mode
480
559
  self._cwd = cwd or self._cwd
481
560
  self._file_edit_mode = file_edit_mode
@@ -546,9 +625,8 @@ def _incremental_text(text: str, last_pending_output: str) -> str:
546
625
  # text = render_terminal_output(text[-100_000:])
547
626
  text = text[-100_000:]
548
627
 
549
- last_pending_output_rendered_lines = render_terminal_output(last_pending_output)
550
- last_pending_output_rendered = "\n".join(last_pending_output_rendered_lines)
551
- last_rendered_lines = last_pending_output_rendered.split("\n")
628
+ last_rendered_lines = render_terminal_output(last_pending_output)
629
+ last_pending_output_rendered = "\n".join(last_rendered_lines)
552
630
  if not last_rendered_lines:
553
631
  return rstrip(render_terminal_output(text))
554
632
 
@@ -599,6 +677,27 @@ def execute_bash(
599
677
  bash_arg: BashCommand,
600
678
  max_tokens: Optional[int],
601
679
  timeout_s: Optional[float],
680
+ ) -> tuple[str, float]:
681
+ try:
682
+ output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
683
+
684
+ # Remove echo if it's a command
685
+ if isinstance(bash_arg.action, Command):
686
+ command = bash_arg.action.command.strip()
687
+ if output.startswith(command):
688
+ output = output[len(command) :]
689
+
690
+ finally:
691
+ bash_state.run_bg_expect_thread()
692
+ return output, cost
693
+
694
+
695
+ def _execute_bash(
696
+ bash_state: BashState,
697
+ enc: EncoderDecoder[int],
698
+ bash_arg: BashCommand,
699
+ max_tokens: Optional[int],
700
+ timeout_s: Optional[float],
602
701
  ) -> tuple[str, float]:
603
702
  try:
604
703
  is_interrupt = False
@@ -102,6 +102,8 @@ def line_process_max_space_tolerance(line: str) -> str:
102
102
  return re.sub(r"\s", "", line)
103
103
 
104
104
 
105
+ REMOVE_INDENTATION = "Warning: matching after removing all spaces in lines."
106
+
105
107
  DEFAULT_TOLERANCES = [
106
108
  Tolerance(
107
109
  line_process=str.rstrip,
@@ -119,11 +121,50 @@ DEFAULT_TOLERANCES = [
119
121
  line_process=line_process_max_space_tolerance,
120
122
  severity_cat="WARNING",
121
123
  score_multiplier=50,
122
- error_name="Warning: matching after removing all spaces in lines.",
124
+ error_name=REMOVE_INDENTATION,
123
125
  ),
124
126
  ]
125
127
 
126
128
 
129
+ def fix_indentation(
130
+ matched_lines: list[str], searched_lines: list[str], replaced_lines: list[str]
131
+ ) -> list[str]:
132
+ if not matched_lines or not searched_lines or not replaced_lines:
133
+ return replaced_lines
134
+
135
+ def get_indentation(line: str) -> str:
136
+ match = re.match(r"^(\s*)", line)
137
+ assert match
138
+ return match.group(0)
139
+
140
+ matched_indents = [get_indentation(line) for line in matched_lines if line.strip()]
141
+ searched_indents = [
142
+ get_indentation(line) for line in searched_lines if line.strip()
143
+ ]
144
+ if len(matched_indents) != len(searched_indents):
145
+ return replaced_lines
146
+ diffs: list[int] = [
147
+ len(searched) - len(matched)
148
+ for matched, searched in zip(matched_indents, searched_indents)
149
+ ]
150
+ if not all(diff == diffs[0] for diff in diffs):
151
+ return replaced_lines
152
+ if diffs[0] == 0:
153
+ return replaced_lines
154
+
155
+ # At this point we have same number of non-empty lines and the same indentation difference
156
+ # We can now adjust the indentation of the replaced lines
157
+ def adjust_indentation(line: str, diff: int) -> str:
158
+ if diff < 0:
159
+ return matched_indents[0][:-diff] + line
160
+ return line[diff:]
161
+
162
+ if diffs[0] > 0:
163
+ if not (all(not line[: diffs[0]].strip() for line in replaced_lines)):
164
+ return replaced_lines
165
+ return [adjust_indentation(line, diffs[0]) for line in replaced_lines]
166
+
167
+
127
168
  def remove_leading_trailing_empty_lines(lines: list[str]) -> list[str]:
128
169
  start = 0
129
170
  end = len(lines) - 1
@@ -247,6 +288,16 @@ class FileEditInput:
247
288
  ]
248
289
 
249
290
  for match, tolerances in matches_with_tolerances:
291
+ if any(
292
+ tolerance.error_name == REMOVE_INDENTATION
293
+ for tolerance in tolerances
294
+ ):
295
+ replace_by = fix_indentation(
296
+ self.file_lines[match.start : match.stop],
297
+ first_block[0],
298
+ replace_by,
299
+ )
300
+
250
301
  file_edit_input = FileEditInput(
251
302
  self.file_lines,
252
303
  match.stop,
@@ -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
@@ -165,7 +157,7 @@ async def main() -> None:
165
157
  version = str(importlib.metadata.version("wcgw"))
166
158
  home_dir = os.path.expanduser("~")
167
159
  with BashState(
168
- Console(), home_dir, None, None, None, None, False, None
160
+ Console(), home_dir, None, None, None, None, True, None
169
161
  ) as BASH_STATE:
170
162
  BASH_STATE.console.log("wcgw version: " + version)
171
163
  # Run the server using stdin/stdout streams