wcgw 4.0.0__py3-none-any.whl → 4.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wcgw might be problematic. Click here for more details.
- wcgw/client/bash_state/bash_state.py +143 -10
- wcgw/client/bash_state/parser/__init__.py +7 -0
- wcgw/client/bash_state/parser/bash_statement_parser.py +181 -0
- wcgw/client/file_ops/diff_edit.py +42 -46
- wcgw/client/file_ops/search_replace.py +74 -55
- wcgw/client/mcp_server/server.py +7 -3
- wcgw/client/modes.py +12 -3
- wcgw/client/repo_ops/repo_context.py +34 -11
- wcgw/client/tool_prompts.py +1 -0
- wcgw/client/tools.py +17 -1
- {wcgw-4.0.0.dist-info → wcgw-4.1.1.dist-info}/METADATA +6 -3
- {wcgw-4.0.0.dist-info → wcgw-4.1.1.dist-info}/RECORD +15 -13
- {wcgw-4.0.0.dist-info → wcgw-4.1.1.dist-info}/WHEEL +0 -0
- {wcgw-4.0.0.dist-info → wcgw-4.1.1.dist-info}/entry_points.txt +0 -0
- {wcgw-4.0.0.dist-info → wcgw-4.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -16,6 +16,7 @@ from typing import (
|
|
|
16
16
|
)
|
|
17
17
|
|
|
18
18
|
import pexpect
|
|
19
|
+
import psutil
|
|
19
20
|
import pyte
|
|
20
21
|
|
|
21
22
|
from ...types_ import (
|
|
@@ -30,6 +31,7 @@ from ...types_ import (
|
|
|
30
31
|
)
|
|
31
32
|
from ..encoder import EncoderDecoder
|
|
32
33
|
from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
|
|
34
|
+
from .parser.bash_statement_parser import BashStatementParser
|
|
33
35
|
|
|
34
36
|
PROMPT_CONST = "wcgw→" + " "
|
|
35
37
|
PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
|
|
@@ -87,6 +89,116 @@ def check_if_screen_command_available() -> bool:
|
|
|
87
89
|
return False
|
|
88
90
|
|
|
89
91
|
|
|
92
|
+
def get_wcgw_screen_sessions() -> list[str]:
|
|
93
|
+
"""
|
|
94
|
+
Get a list of all WCGW screen session IDs.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of screen session IDs that match the wcgw pattern.
|
|
98
|
+
"""
|
|
99
|
+
screen_sessions = []
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Get list of all screen sessions
|
|
103
|
+
result = subprocess.run(
|
|
104
|
+
["screen", "-ls"],
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
check=False, # Don't raise exception on non-zero exit code
|
|
108
|
+
timeout=0.5,
|
|
109
|
+
)
|
|
110
|
+
output = result.stdout or result.stderr or ""
|
|
111
|
+
|
|
112
|
+
# Parse screen output to get session IDs
|
|
113
|
+
for line in output.splitlines():
|
|
114
|
+
line = line.strip()
|
|
115
|
+
if not line or not line[0].isdigit():
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Extract session info (e.g., "1234.wcgw.123456 (Detached)")
|
|
119
|
+
session_parts = line.split()
|
|
120
|
+
if not session_parts:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
session_id = session_parts[0].strip()
|
|
124
|
+
|
|
125
|
+
# Check if it's a WCGW session
|
|
126
|
+
if ".wcgw." in session_id:
|
|
127
|
+
screen_sessions.append(session_id)
|
|
128
|
+
except Exception:
|
|
129
|
+
# If anything goes wrong, just return empty list
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
return screen_sessions
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_orphaned_wcgw_screens() -> list[str]:
|
|
136
|
+
"""
|
|
137
|
+
Identify orphaned WCGW screen sessions where the parent process has PID 1
|
|
138
|
+
or doesn't exist.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of screen session IDs that are orphaned and match the wcgw pattern.
|
|
142
|
+
"""
|
|
143
|
+
orphaned_screens = []
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
# Get list of all WCGW screen sessions
|
|
147
|
+
screen_sessions = get_wcgw_screen_sessions()
|
|
148
|
+
|
|
149
|
+
for session_id in screen_sessions:
|
|
150
|
+
# Extract PID from session ID (first part before the dot)
|
|
151
|
+
try:
|
|
152
|
+
pid = int(session_id.split(".")[0])
|
|
153
|
+
|
|
154
|
+
# Check if process exists and if its parent is PID 1
|
|
155
|
+
try:
|
|
156
|
+
process = psutil.Process(pid)
|
|
157
|
+
parent_pid = process.ppid()
|
|
158
|
+
|
|
159
|
+
if parent_pid == 1:
|
|
160
|
+
# This is an orphaned process
|
|
161
|
+
orphaned_screens.append(session_id)
|
|
162
|
+
except psutil.NoSuchProcess:
|
|
163
|
+
# Process doesn't exist anymore, consider it orphaned
|
|
164
|
+
orphaned_screens.append(session_id)
|
|
165
|
+
except (ValueError, IndexError):
|
|
166
|
+
# Couldn't parse PID, skip
|
|
167
|
+
continue
|
|
168
|
+
except Exception:
|
|
169
|
+
# If anything goes wrong, just return empty list
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
return orphaned_screens
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def cleanup_orphaned_wcgw_screens(console: Console) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Clean up all orphaned WCGW screen sessions.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
console: Console for logging.
|
|
181
|
+
"""
|
|
182
|
+
orphaned_sessions = get_orphaned_wcgw_screens()
|
|
183
|
+
|
|
184
|
+
if not orphaned_sessions:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
f"Found {len(orphaned_sessions)} orphaned WCGW screen sessions to clean up"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
for session in orphaned_sessions:
|
|
192
|
+
try:
|
|
193
|
+
subprocess.run(
|
|
194
|
+
["screen", "-S", session, "-X", "quit"],
|
|
195
|
+
check=False,
|
|
196
|
+
timeout=CONFIG.timeout,
|
|
197
|
+
)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
console.log(f"Failed to kill orphaned screen session: {session}\n{e}")
|
|
200
|
+
|
|
201
|
+
|
|
90
202
|
def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
91
203
|
"""
|
|
92
204
|
There could be in worst case multiple screens with same name, clear them if any.
|
|
@@ -125,7 +237,6 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
125
237
|
session_info = line.split()[0].strip() # e.g., "1234.my_screen"
|
|
126
238
|
if session_info.endswith(f".{name}"):
|
|
127
239
|
sessions_to_kill.append(session_info)
|
|
128
|
-
|
|
129
240
|
# Now, for every session we found, tell screen to quit it.
|
|
130
241
|
for session in sessions_to_kill:
|
|
131
242
|
try:
|
|
@@ -258,13 +369,20 @@ class BashState:
|
|
|
258
369
|
|
|
259
370
|
def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
|
|
260
371
|
self.close_bg_expect_thread()
|
|
261
|
-
|
|
372
|
+
try:
|
|
373
|
+
output = self._shell.expect(pattern, timeout)
|
|
374
|
+
except pexpect.TIMEOUT:
|
|
375
|
+
# Edge case: gets raised when the child fd is not ready in some timeout
|
|
376
|
+
# pexpect/utils.py:143
|
|
377
|
+
return 1
|
|
262
378
|
return output
|
|
263
379
|
|
|
264
380
|
def send(self, s: str | bytes, set_as_command: Optional[str]) -> int:
|
|
265
381
|
self.close_bg_expect_thread()
|
|
266
382
|
if set_as_command is not None:
|
|
267
383
|
self._last_command = set_as_command
|
|
384
|
+
# if s == "\n":
|
|
385
|
+
# return self._shell.sendcontrol("m")
|
|
268
386
|
output = self._shell.send(s)
|
|
269
387
|
return output
|
|
270
388
|
|
|
@@ -319,11 +437,9 @@ class BashState:
|
|
|
319
437
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
320
438
|
|
|
321
439
|
def cleanup(self) -> None:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
finally:
|
|
326
|
-
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
440
|
+
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
441
|
+
self.close_bg_expect_thread()
|
|
442
|
+
self._shell.close(True)
|
|
327
443
|
|
|
328
444
|
def __enter__(self) -> "BashState":
|
|
329
445
|
return self
|
|
@@ -385,6 +501,11 @@ class BashState:
|
|
|
385
501
|
self._last_command = ""
|
|
386
502
|
# Ensure self._cwd exists
|
|
387
503
|
os.makedirs(self._cwd, exist_ok=True)
|
|
504
|
+
|
|
505
|
+
# Clean up orphaned WCGW screen sessions
|
|
506
|
+
if check_if_screen_command_available():
|
|
507
|
+
cleanup_orphaned_wcgw_screens(self.console)
|
|
508
|
+
|
|
388
509
|
try:
|
|
389
510
|
self._shell, self._shell_id = start_shell(
|
|
390
511
|
self._bash_command_mode.bash_mode == "restricted_mode",
|
|
@@ -815,10 +936,22 @@ def _execute_bash(
|
|
|
815
936
|
raise ValueError(WAITING_INPUT_MESSAGE)
|
|
816
937
|
|
|
817
938
|
command = command_data.command.strip()
|
|
939
|
+
|
|
940
|
+
# Check for multiple statements using the bash statement parser
|
|
818
941
|
if "\n" in command:
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
942
|
+
try:
|
|
943
|
+
parser = BashStatementParser()
|
|
944
|
+
statements = parser.parse_string(command)
|
|
945
|
+
if len(statements) > 1:
|
|
946
|
+
return (
|
|
947
|
+
"Error: Command contains multiple statements. Please run only one bash statement at a time.",
|
|
948
|
+
0.0,
|
|
949
|
+
)
|
|
950
|
+
except Exception:
|
|
951
|
+
# Fall back to simple newline check if something goes wrong
|
|
952
|
+
raise ValueError(
|
|
953
|
+
"Command should not contain newline character in middle. Run only one command at a time."
|
|
954
|
+
)
|
|
822
955
|
|
|
823
956
|
for i in range(0, len(command), 128):
|
|
824
957
|
bash_state.send(command[i : i + 128], set_as_command=None)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Bash Statement Parser
|
|
4
|
+
|
|
5
|
+
This script parses bash scripts and identifies individual statements using tree-sitter.
|
|
6
|
+
It correctly handles multi-line strings, command chains with && and ||, and semicolon-separated statements.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
import tree_sitter_bash
|
|
14
|
+
from tree_sitter import Language, Parser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Statement:
|
|
19
|
+
"""A bash statement with its source code and position information."""
|
|
20
|
+
|
|
21
|
+
text: str
|
|
22
|
+
start_line: int
|
|
23
|
+
end_line: int
|
|
24
|
+
start_byte: int
|
|
25
|
+
end_byte: int
|
|
26
|
+
node_type: str
|
|
27
|
+
parent_type: Optional[str] = None
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return self.text.strip()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BashStatementParser:
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
# Use the precompiled bash language
|
|
36
|
+
self.language = Language(tree_sitter_bash.language())
|
|
37
|
+
self.parser = Parser(self.language)
|
|
38
|
+
|
|
39
|
+
def parse_file(self, file_path: str) -> List[Statement]:
|
|
40
|
+
"""Parse a bash script file and return a list of statements."""
|
|
41
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
42
|
+
content = f.read()
|
|
43
|
+
return self.parse_string(content)
|
|
44
|
+
|
|
45
|
+
def parse_string(self, content: str) -> List[Statement]:
|
|
46
|
+
"""Parse a string containing bash script and return a list of statements."""
|
|
47
|
+
tree = self.parser.parse(bytes(content, "utf-8"))
|
|
48
|
+
root_node = tree.root_node
|
|
49
|
+
|
|
50
|
+
# For debugging: Uncomment to print the tree structure
|
|
51
|
+
# self._print_tree(root_node, content)
|
|
52
|
+
|
|
53
|
+
statements: List[Statement] = []
|
|
54
|
+
self._extract_statements(root_node, content, statements, None)
|
|
55
|
+
|
|
56
|
+
# Post-process statements to handle multi-line statements correctly
|
|
57
|
+
return self._post_process_statements(statements, content)
|
|
58
|
+
|
|
59
|
+
def _print_tree(self, node: Any, content: str, indent: str = "") -> None:
|
|
60
|
+
"""Debug helper to print the entire syntax tree."""
|
|
61
|
+
node_text = content[node.start_byte : node.end_byte]
|
|
62
|
+
if len(node_text) > 40:
|
|
63
|
+
node_text = node_text[:37] + "..."
|
|
64
|
+
print(f"{indent}{node.type}: {repr(node_text)}")
|
|
65
|
+
for child in node.children:
|
|
66
|
+
self._print_tree(child, content, indent + " ")
|
|
67
|
+
|
|
68
|
+
def _extract_statements(
|
|
69
|
+
self,
|
|
70
|
+
node: Any,
|
|
71
|
+
content: str,
|
|
72
|
+
statements: List[Statement],
|
|
73
|
+
parent_type: Optional[str],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Recursively extract statements from the syntax tree."""
|
|
76
|
+
# Node types that represent bash statements
|
|
77
|
+
statement_node_types = {
|
|
78
|
+
# Basic statements
|
|
79
|
+
"command",
|
|
80
|
+
"variable_assignment",
|
|
81
|
+
"declaration_command",
|
|
82
|
+
"unset_command",
|
|
83
|
+
# Control flow statements
|
|
84
|
+
"for_statement",
|
|
85
|
+
"c_style_for_statement",
|
|
86
|
+
"while_statement",
|
|
87
|
+
"if_statement",
|
|
88
|
+
"case_statement",
|
|
89
|
+
# Function definition
|
|
90
|
+
"function_definition",
|
|
91
|
+
# Command chains and groups
|
|
92
|
+
"pipeline", # For command chains with | and |&
|
|
93
|
+
"list", # For command chains with && and ||
|
|
94
|
+
"compound_statement",
|
|
95
|
+
"subshell",
|
|
96
|
+
"redirected_statement",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Create a Statement object for this node if it's a recognized statement type
|
|
100
|
+
if node.type in statement_node_types:
|
|
101
|
+
# Get the text of this statement
|
|
102
|
+
start_byte = node.start_byte
|
|
103
|
+
end_byte = node.end_byte
|
|
104
|
+
statement_text = content[start_byte:end_byte]
|
|
105
|
+
|
|
106
|
+
# Get line numbers
|
|
107
|
+
start_line = (
|
|
108
|
+
node.start_point[0] + 1
|
|
109
|
+
) # tree-sitter uses 0-indexed line numbers
|
|
110
|
+
end_line = node.end_point[0] + 1
|
|
111
|
+
|
|
112
|
+
statements.append(
|
|
113
|
+
Statement(
|
|
114
|
+
text=statement_text,
|
|
115
|
+
start_line=start_line,
|
|
116
|
+
end_line=end_line,
|
|
117
|
+
start_byte=start_byte,
|
|
118
|
+
end_byte=end_byte,
|
|
119
|
+
node_type=node.type,
|
|
120
|
+
parent_type=parent_type,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Update parent type for children
|
|
125
|
+
parent_type = node.type
|
|
126
|
+
|
|
127
|
+
# Recursively process all children
|
|
128
|
+
for child in node.children:
|
|
129
|
+
self._extract_statements(child, content, statements, parent_type)
|
|
130
|
+
|
|
131
|
+
def _post_process_statements(
|
|
132
|
+
self, statements: List[Statement], content: str
|
|
133
|
+
) -> List[Statement]:
|
|
134
|
+
if not statements:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
# Filter out list statements that have been split
|
|
138
|
+
top_statements = []
|
|
139
|
+
for stmt in statements:
|
|
140
|
+
# Skip statements that are contained within others
|
|
141
|
+
is_contained = False
|
|
142
|
+
for other in statements:
|
|
143
|
+
if other is stmt:
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
# Check if completely contained (except for lists we've split)
|
|
147
|
+
if other.node_type != "list" or ";" not in other.text:
|
|
148
|
+
if (
|
|
149
|
+
other.start_line <= stmt.start_line
|
|
150
|
+
and other.end_line >= stmt.end_line
|
|
151
|
+
and len(other.text) > len(stmt.text)
|
|
152
|
+
and stmt.text in other.text
|
|
153
|
+
):
|
|
154
|
+
is_contained = True
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if not is_contained:
|
|
158
|
+
top_statements.append(stmt)
|
|
159
|
+
|
|
160
|
+
# Sort by position in file for consistent output
|
|
161
|
+
top_statements.sort(key=lambda s: (s.start_line, s.text))
|
|
162
|
+
|
|
163
|
+
return top_statements
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def main() -> None:
|
|
167
|
+
if len(sys.argv) < 2:
|
|
168
|
+
print("Usage: python bash_statement_parser.py <bash_script_file>")
|
|
169
|
+
sys.exit(1)
|
|
170
|
+
|
|
171
|
+
parser = BashStatementParser()
|
|
172
|
+
statements = parser.parse_file(sys.argv[1])
|
|
173
|
+
|
|
174
|
+
print(f"Found {len(statements)} statements:")
|
|
175
|
+
for i, stmt in enumerate(statements, 1):
|
|
176
|
+
print(f"\n--- Statement {i} (Lines {stmt.start_line}-{stmt.end_line}) ---")
|
|
177
|
+
print(stmt)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
main()
|
|
@@ -7,7 +7,13 @@ TOLERANCE_TYPES = Literal["SILENT", "WARNING", "ERROR"]
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class SearchReplaceMatchError(Exception):
|
|
10
|
-
|
|
10
|
+
def __init__(self, message: str):
|
|
11
|
+
message = f"""
|
|
12
|
+
{message}
|
|
13
|
+
---
|
|
14
|
+
Retry immediately with same "percentage_to_change" using search replace blocks fixing above error.
|
|
15
|
+
"""
|
|
16
|
+
super().__init__(message)
|
|
11
17
|
|
|
12
18
|
|
|
13
19
|
@dataclass
|
|
@@ -27,7 +33,9 @@ class TolerancesHit(Tolerance):
|
|
|
27
33
|
class FileEditOutput:
|
|
28
34
|
original_content: list[str]
|
|
29
35
|
orig_search_blocks: list[list[str]]
|
|
30
|
-
edited_with_tolerances: list[
|
|
36
|
+
edited_with_tolerances: list[
|
|
37
|
+
tuple[slice, list[TolerancesHit], list[str]]
|
|
38
|
+
] # Need not be equal to orig_search_blocks when early exit
|
|
31
39
|
|
|
32
40
|
def replace_or_throw(
|
|
33
41
|
self,
|
|
@@ -94,8 +102,7 @@ Error:
|
|
|
94
102
|
best_score = hit_score
|
|
95
103
|
elif abs(hit_score - best_score) < 1e-3:
|
|
96
104
|
best_hits.append(output)
|
|
97
|
-
|
|
98
|
-
return best_hits, best_score < 0
|
|
105
|
+
return best_hits, best_score > 1000
|
|
99
106
|
|
|
100
107
|
|
|
101
108
|
def line_process_max_space_tolerance(line: str) -> str:
|
|
@@ -205,7 +212,7 @@ class FileEditInput:
|
|
|
205
212
|
TolerancesHit(
|
|
206
213
|
line_process=lambda x: x,
|
|
207
214
|
severity_cat="ERROR",
|
|
208
|
-
score_multiplier=float("
|
|
215
|
+
score_multiplier=float("inf"),
|
|
209
216
|
error_name="The blocks couldn't be matched, maybe the sequence of search blocks was incorrect?",
|
|
210
217
|
count=max(1, len(search_lines)),
|
|
211
218
|
)
|
|
@@ -241,6 +248,7 @@ class FileEditInput:
|
|
|
241
248
|
|
|
242
249
|
# search for first block
|
|
243
250
|
first_block = self.search_replace_blocks[self.search_replace_offset]
|
|
251
|
+
replace_by = first_block[1]
|
|
244
252
|
|
|
245
253
|
# Try exact match
|
|
246
254
|
matches = match_exact(self.file_lines, self.file_line_offset, first_block[0])
|
|
@@ -252,7 +260,6 @@ class FileEditInput:
|
|
|
252
260
|
matches_with_tolerances = match_with_tolerance(
|
|
253
261
|
self.file_lines, self.file_line_offset, first_block[0], self.tolerances
|
|
254
262
|
)
|
|
255
|
-
replace_by = first_block[1]
|
|
256
263
|
if not matches_with_tolerances:
|
|
257
264
|
# Try with no empty lines
|
|
258
265
|
matches_with_tolerances = match_with_tolerance_empty_line(
|
|
@@ -278,8 +285,8 @@ class FileEditInput:
|
|
|
278
285
|
TolerancesHit(
|
|
279
286
|
lambda x: x,
|
|
280
287
|
"ERROR",
|
|
281
|
-
|
|
282
|
-
"Couldn't find match.
|
|
288
|
+
float("inf"),
|
|
289
|
+
"Couldn't find match. Here's the latest snippet from the file which might be relevant for you to consider:\n```"
|
|
283
290
|
+ sim_context
|
|
284
291
|
+ "\n```",
|
|
285
292
|
int(len(first_block[0]) // sim_sim),
|
|
@@ -288,51 +295,40 @@ class FileEditInput:
|
|
|
288
295
|
)
|
|
289
296
|
]
|
|
290
297
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
file_edit_input = FileEditInput(
|
|
303
|
-
self.file_lines,
|
|
304
|
-
match.stop,
|
|
305
|
-
self.search_replace_blocks,
|
|
306
|
-
self.search_replace_offset + 1,
|
|
307
|
-
self.tolerances,
|
|
298
|
+
else:
|
|
299
|
+
matches_with_tolerances = [(match, []) for match in matches]
|
|
300
|
+
|
|
301
|
+
for match, tolerances in matches_with_tolerances:
|
|
302
|
+
if any(
|
|
303
|
+
tolerance.error_name == REMOVE_INDENTATION for tolerance in tolerances
|
|
304
|
+
):
|
|
305
|
+
replace_by = fix_indentation(
|
|
306
|
+
self.file_lines[match.start : match.stop],
|
|
307
|
+
first_block[0],
|
|
308
|
+
replace_by,
|
|
308
309
|
)
|
|
309
310
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
self.search_replace_offset + 1,
|
|
325
|
-
self.tolerances,
|
|
311
|
+
file_edit_input = FileEditInput(
|
|
312
|
+
self.file_lines,
|
|
313
|
+
match.stop,
|
|
314
|
+
self.search_replace_blocks,
|
|
315
|
+
self.search_replace_offset + 1,
|
|
316
|
+
self.tolerances,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if any(tolerance.severity_cat == "ERROR" for tolerance in tolerances):
|
|
320
|
+
# Exit early
|
|
321
|
+
all_outputs.append(
|
|
322
|
+
[
|
|
323
|
+
(match, tolerances, replace_by),
|
|
324
|
+
]
|
|
326
325
|
)
|
|
326
|
+
else:
|
|
327
327
|
remaining_output = file_edit_input.edit_file()
|
|
328
328
|
for rem_output in remaining_output:
|
|
329
329
|
all_outputs.append(
|
|
330
330
|
[
|
|
331
|
-
(
|
|
332
|
-
match,
|
|
333
|
-
[],
|
|
334
|
-
first_block[1],
|
|
335
|
-
),
|
|
331
|
+
(match, tolerances, replace_by),
|
|
336
332
|
*rem_output.edited_with_tolerances,
|
|
337
333
|
]
|
|
338
334
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
|
-
from typing import Callable
|
|
2
|
+
from typing import Callable, Optional
|
|
3
3
|
|
|
4
4
|
from .diff_edit import FileEditInput, FileEditOutput, SearchReplaceMatchError
|
|
5
5
|
|
|
@@ -100,9 +100,10 @@ def search_replace_edit(
|
|
|
100
100
|
"No valid search replace blocks found, ensure your SEARCH/REPLACE blocks are formatted correctly"
|
|
101
101
|
)
|
|
102
102
|
|
|
103
|
-
edited_content, comments_ =
|
|
104
|
-
original_lines,
|
|
103
|
+
edited_content, comments_ = edit_with_individual_fallback(
|
|
104
|
+
original_lines, search_replace_blocks
|
|
105
105
|
)
|
|
106
|
+
|
|
106
107
|
edited_file = "\n".join(edited_content)
|
|
107
108
|
if not comments_:
|
|
108
109
|
comments = "Edited successfully"
|
|
@@ -114,63 +115,81 @@ def search_replace_edit(
|
|
|
114
115
|
return edited_file, comments
|
|
115
116
|
|
|
116
117
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if
|
|
125
|
-
return
|
|
126
|
-
|
|
118
|
+
def identify_first_differing_block(
|
|
119
|
+
best_matches: list[FileEditOutput],
|
|
120
|
+
) -> Optional[list[str]]:
|
|
121
|
+
"""
|
|
122
|
+
Identify the first search block that differs across multiple best matches.
|
|
123
|
+
Returns the search block content that first shows different matches.
|
|
124
|
+
"""
|
|
125
|
+
if not best_matches or len(best_matches) <= 1:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
# First, check if the number of blocks differs (shouldn't happen, but let's be safe)
|
|
129
|
+
block_counts = [len(match.edited_with_tolerances) for match in best_matches]
|
|
130
|
+
if not all(count == block_counts[0] for count in block_counts):
|
|
131
|
+
# If block counts differ, just return the first search block as problematic
|
|
132
|
+
return (
|
|
133
|
+
best_matches[0].orig_search_blocks[0]
|
|
134
|
+
if best_matches[0].orig_search_blocks
|
|
135
|
+
else None
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Go through each block position and see if the slices differ
|
|
139
|
+
for i in range(min(block_counts)):
|
|
140
|
+
slices = [match.edited_with_tolerances[i][0] for match in best_matches]
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
# Check if we have different slices for this block across matches
|
|
143
|
+
if any(s.start != slices[0].start or s.stop != slices[0].stop for s in slices):
|
|
144
|
+
# We found our differing block - return the search block content
|
|
145
|
+
if i < len(best_matches[0].orig_search_blocks):
|
|
146
|
+
return best_matches[0].orig_search_blocks[i]
|
|
147
|
+
else:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# If we get here, we couldn't identify a specific differing block
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def edit_with_individual_fallback(
|
|
155
|
+
original_lines: list[str], search_replace_blocks: list[tuple[list[str], list[str]]]
|
|
156
|
+
) -> tuple[list[str], set[str]]:
|
|
157
|
+
outputs = FileEditInput(original_lines, 0, search_replace_blocks, 0).edit_file()
|
|
129
158
|
best_matches, is_error = FileEditOutput.get_best_match(outputs)
|
|
130
159
|
|
|
131
|
-
|
|
132
|
-
best_matches[0].replace_or_throw(3)
|
|
133
|
-
|
|
160
|
+
try:
|
|
161
|
+
edited_content, comments_ = best_matches[0].replace_or_throw(3)
|
|
162
|
+
except SearchReplaceMatchError:
|
|
163
|
+
if len(search_replace_blocks) > 1:
|
|
164
|
+
# Try one at a time
|
|
165
|
+
all_comments = set[str]()
|
|
166
|
+
running_lines = list(original_lines)
|
|
167
|
+
for block in search_replace_blocks:
|
|
168
|
+
running_lines, comments_ = edit_with_individual_fallback(
|
|
169
|
+
running_lines, [block]
|
|
170
|
+
)
|
|
171
|
+
all_comments |= comments_
|
|
172
|
+
return running_lines, all_comments
|
|
173
|
+
raise
|
|
174
|
+
assert not is_error
|
|
134
175
|
|
|
135
176
|
if len(best_matches) > 1:
|
|
136
|
-
#
|
|
137
|
-
|
|
138
|
-
|
|
177
|
+
# Find the first block that differs across matches
|
|
178
|
+
first_diff_block = identify_first_differing_block(best_matches)
|
|
179
|
+
if first_diff_block is not None:
|
|
180
|
+
block_content = "\n".join(first_diff_block)
|
|
139
181
|
raise SearchReplaceMatchError(f"""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
182
|
+
The following block matched more than once:
|
|
183
|
+
```
|
|
184
|
+
{block_content}
|
|
185
|
+
```
|
|
186
|
+
Consider adding more context before and after this block to make the match unique.
|
|
145
187
|
""")
|
|
146
|
-
|
|
147
188
|
else:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
original_lines, search_replace_blocks, original_lines, set(), 0
|
|
156
|
-
)
|
|
157
|
-
except Exception:
|
|
158
|
-
ma_more = "\n".join(current_blocks[-1][0])
|
|
159
|
-
raise Exception(f"""
|
|
160
|
-
The following block matched more than once:
|
|
161
|
-
---
|
|
162
|
-
```
|
|
163
|
-
{ma_more}
|
|
164
|
-
```
|
|
165
|
-
""")
|
|
166
|
-
|
|
167
|
-
best_match = best_matches[0]
|
|
168
|
-
running_lines, comments = best_match.replace_or_throw(3)
|
|
169
|
-
running_comments = running_comments | comments
|
|
170
|
-
return greedy_context_replace(
|
|
171
|
-
original_lines,
|
|
172
|
-
search_replace_blocks,
|
|
173
|
-
running_lines,
|
|
174
|
-
running_comments,
|
|
175
|
-
current_block_offset + 1,
|
|
176
|
-
)
|
|
189
|
+
raise SearchReplaceMatchError("""
|
|
190
|
+
One of the blocks matched more than once
|
|
191
|
+
|
|
192
|
+
Consider adding more context before and after all the blocks to make the match unique.
|
|
193
|
+
""")
|
|
194
|
+
|
|
195
|
+
return edited_content, comments_
|
wcgw/client/mcp_server/server.py
CHANGED
|
@@ -16,7 +16,7 @@ from wcgw.client.tool_prompts import TOOL_PROMPTS
|
|
|
16
16
|
from ...types_ import (
|
|
17
17
|
Initialize,
|
|
18
18
|
)
|
|
19
|
-
from ..bash_state.bash_state import CONFIG, BashState
|
|
19
|
+
from ..bash_state.bash_state import CONFIG, BashState, get_tmpdir
|
|
20
20
|
from ..tools import (
|
|
21
21
|
Context,
|
|
22
22
|
default_enc,
|
|
@@ -155,9 +155,13 @@ async def main() -> None:
|
|
|
155
155
|
global BASH_STATE
|
|
156
156
|
CONFIG.update(3, 55, 5)
|
|
157
157
|
version = str(importlib.metadata.version("wcgw"))
|
|
158
|
-
|
|
158
|
+
|
|
159
|
+
# starting_dir is inside tmp dir
|
|
160
|
+
tmp_dir = get_tmpdir()
|
|
161
|
+
starting_dir = os.path.join(tmp_dir, "claude_playground")
|
|
162
|
+
|
|
159
163
|
with BashState(
|
|
160
|
-
Console(),
|
|
164
|
+
Console(), starting_dir, None, None, None, None, True, None
|
|
161
165
|
) as BASH_STATE:
|
|
162
166
|
BASH_STATE.console.log("wcgw version: " + version)
|
|
163
167
|
# Run the server using stdin/stdout streams
|
wcgw/client/modes.py
CHANGED
|
@@ -125,8 +125,9 @@ Instructions:
|
|
|
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
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
|
-
- Do not use echo to write multi-line files, always use
|
|
129
|
-
|
|
128
|
+
- Do not use echo to write multi-line files, always use FileWriteOrEdit tool to update a code.
|
|
129
|
+
- Provide as many file paths as you need in ReadFiles in one go.
|
|
130
|
+
|
|
130
131
|
Additional instructions:
|
|
131
132
|
Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
|
|
132
133
|
|
|
@@ -134,18 +135,26 @@ Additional instructions:
|
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
"""
|
|
137
|
-
ARCHITECT_PROMPT = """
|
|
138
|
+
ARCHITECT_PROMPT = """
|
|
139
|
+
# Instructions
|
|
140
|
+
You are now running in "architect" mode. This means
|
|
138
141
|
- You are not allowed to edit or update any file. You are not allowed to create any file.
|
|
139
142
|
- You are not allowed to run any commands that may change disk, system configuration, packages or environment. Only read-only commands are allowed.
|
|
140
143
|
- Only run commands that allows you to explore the repository, understand the system or read anything of relevance.
|
|
141
144
|
- 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
145
|
- You are not allowed to change directory (bash will run in -r mode)
|
|
143
146
|
- Share only snippets when any implementation is requested.
|
|
147
|
+
- Provide as many file paths as you need in ReadFiles in one go.
|
|
148
|
+
|
|
149
|
+
# Disallowed tools (important!)
|
|
150
|
+
- FileWriteOrEdit
|
|
144
151
|
|
|
152
|
+
# Response instructions
|
|
145
153
|
Respond only after doing the following:
|
|
146
154
|
- Read as many relevant files as possible.
|
|
147
155
|
- Be comprehensive in your understanding and search of relevant files.
|
|
148
156
|
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
157
|
+
- Share minimal snippets higlighting the changes (avoid large number of lines in the snippets, use ... comments)
|
|
149
158
|
"""
|
|
150
159
|
|
|
151
160
|
|
|
@@ -141,7 +141,22 @@ def get_recent_git_files(repo: Repository, count: int = 10) -> list[str]:
|
|
|
141
141
|
return recent_files
|
|
142
142
|
|
|
143
143
|
|
|
144
|
-
def
|
|
144
|
+
def calculate_dynamic_file_limit(total_files: int) -> int:
|
|
145
|
+
# Scale linearly, with minimum and maximum bounds
|
|
146
|
+
min_files = 50
|
|
147
|
+
max_files = 400
|
|
148
|
+
|
|
149
|
+
if total_files <= min_files:
|
|
150
|
+
return min_files
|
|
151
|
+
|
|
152
|
+
scale_factor = (max_files - min_files) / (30000 - min_files)
|
|
153
|
+
|
|
154
|
+
dynamic_limit = min_files + int((total_files - min_files) * scale_factor)
|
|
155
|
+
|
|
156
|
+
return min(max_files, dynamic_limit)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_repo_context(file_or_repo_path: str) -> tuple[str, Path]:
|
|
145
160
|
file_or_repo_path_ = Path(file_or_repo_path).absolute()
|
|
146
161
|
|
|
147
162
|
repo = find_ancestor_with_git(file_or_repo_path_)
|
|
@@ -150,9 +165,6 @@ def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]
|
|
|
150
165
|
# Determine the context directory
|
|
151
166
|
if repo is not None:
|
|
152
167
|
context_dir = Path(repo.path).parent
|
|
153
|
-
# Get recent git files - get at least 50 or the max_files count, whichever is larger
|
|
154
|
-
recent_files_count = max(10, max_files)
|
|
155
|
-
recent_git_files = get_recent_git_files(repo, recent_files_count)
|
|
156
168
|
else:
|
|
157
169
|
if file_or_repo_path_.is_file():
|
|
158
170
|
context_dir = file_or_repo_path_.parent
|
|
@@ -162,8 +174,19 @@ def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]
|
|
|
162
174
|
# Load workspace stats from the context directory
|
|
163
175
|
workspace_stats = load_workspace_stats(str(context_dir))
|
|
164
176
|
|
|
177
|
+
# Get all files and calculate dynamic max files limit once
|
|
165
178
|
all_files = get_all_files_max_depth(str(context_dir), 10, repo)
|
|
166
179
|
|
|
180
|
+
# For Git repositories, get recent files
|
|
181
|
+
if repo is not None:
|
|
182
|
+
dynamic_max_files = calculate_dynamic_file_limit(len(all_files))
|
|
183
|
+
# Get recent git files - get at least 10 or 20% of dynamic_max_files, whichever is larger
|
|
184
|
+
recent_files_count = max(10, int(dynamic_max_files * 0.2))
|
|
185
|
+
recent_git_files = get_recent_git_files(repo, recent_files_count)
|
|
186
|
+
else:
|
|
187
|
+
# We don't want dynamic limit for non git folders like /tmp or ~
|
|
188
|
+
dynamic_max_files = 50
|
|
189
|
+
|
|
167
190
|
# Calculate probabilities in batch
|
|
168
191
|
path_scores = PATH_SCORER.calculate_path_probabilities_batch(all_files)
|
|
169
192
|
|
|
@@ -218,16 +241,16 @@ def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]
|
|
|
218
241
|
if file not in top_files and file in all_files:
|
|
219
242
|
top_files.append(file)
|
|
220
243
|
|
|
221
|
-
# Use statistical sorting for the remaining files, but respect
|
|
244
|
+
# Use statistical sorting for the remaining files, but respect dynamic_max_files limit
|
|
222
245
|
# and ensure we don't add duplicates
|
|
223
|
-
if len(top_files) <
|
|
246
|
+
if len(top_files) < dynamic_max_files:
|
|
224
247
|
# Only add statistically important files that aren't already in top_files
|
|
225
248
|
for file in sorted_files:
|
|
226
|
-
if file not in top_files and len(top_files) <
|
|
249
|
+
if file not in top_files and len(top_files) < dynamic_max_files:
|
|
227
250
|
top_files.append(file)
|
|
228
251
|
|
|
229
|
-
directory_printer = DirectoryTree(context_dir, max_files=
|
|
230
|
-
for file in top_files[:
|
|
252
|
+
directory_printer = DirectoryTree(context_dir, max_files=dynamic_max_files)
|
|
253
|
+
for file in top_files[:dynamic_max_files]:
|
|
231
254
|
directory_printer.expand(file)
|
|
232
255
|
|
|
233
256
|
return directory_printer.display(), context_dir
|
|
@@ -245,7 +268,7 @@ if __name__ == "__main__":
|
|
|
245
268
|
# Profile using cProfile for overall function statistics
|
|
246
269
|
profiler = cProfile.Profile()
|
|
247
270
|
profiler.enable()
|
|
248
|
-
result = get_repo_context(folder
|
|
271
|
+
result = get_repo_context(folder)[0]
|
|
249
272
|
profiler.disable()
|
|
250
273
|
|
|
251
274
|
# Print cProfile stats
|
|
@@ -257,7 +280,7 @@ if __name__ == "__main__":
|
|
|
257
280
|
# Profile using line_profiler for line-by-line statistics
|
|
258
281
|
lp = LineProfiler()
|
|
259
282
|
lp_wrapper = lp(get_repo_context)
|
|
260
|
-
lp_wrapper(folder
|
|
283
|
+
lp_wrapper(folder)
|
|
261
284
|
|
|
262
285
|
print("\n=== Line-by-line profiling ===")
|
|
263
286
|
lp.print_stats()
|
wcgw/client/tool_prompts.py
CHANGED
|
@@ -77,6 +77,7 @@ TOOL_PROMPTS = [
|
|
|
77
77
|
description="""
|
|
78
78
|
- Writes or edits a file based on the percentage of changes.
|
|
79
79
|
- Use absolute path only (~ allowed).
|
|
80
|
+
- percentage_to_change is calculated as number of existing lines that will have some diff divided by total existing lines.
|
|
80
81
|
- First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change
|
|
81
82
|
- percentage_to_change should be low if mostly new code is to be added. It should be high if a lot of things are to be replaced.
|
|
82
83
|
- If percentage_to_change > 50, provide full file content in file_content_or_search_replace_blocks
|
wcgw/client/tools.py
CHANGED
|
@@ -142,7 +142,7 @@ def initialize(
|
|
|
142
142
|
read_files_ = [any_workspace_path]
|
|
143
143
|
any_workspace_path = os.path.dirname(any_workspace_path)
|
|
144
144
|
# Let get_repo_context handle loading the workspace stats
|
|
145
|
-
repo_context, folder_to_start = get_repo_context(any_workspace_path
|
|
145
|
+
repo_context, folder_to_start = get_repo_context(any_workspace_path)
|
|
146
146
|
|
|
147
147
|
repo_context = f"---\n# Workspace structure\n{repo_context}\n---\n"
|
|
148
148
|
|
|
@@ -229,6 +229,20 @@ def initialize(
|
|
|
229
229
|
)
|
|
230
230
|
initial_files_context = f"---\n# Requested files\n{initial_files}\n---\n"
|
|
231
231
|
|
|
232
|
+
# Check for CLAUDE.md in the workspace folder on first call
|
|
233
|
+
alignment_context = ""
|
|
234
|
+
if folder_to_start:
|
|
235
|
+
alignment_file_path = os.path.join(folder_to_start, "CLAUDE.md")
|
|
236
|
+
if os.path.exists(alignment_file_path):
|
|
237
|
+
try:
|
|
238
|
+
# Read the CLAUDE.md file content
|
|
239
|
+
with open(alignment_file_path, "r") as f:
|
|
240
|
+
alignment_content = f.read()
|
|
241
|
+
alignment_context = f"---\n# CLAUDE.md - Project alignment guidelines\n```\n{alignment_content}\n```\n---\n\n"
|
|
242
|
+
except Exception:
|
|
243
|
+
# Handle any errors when reading the file
|
|
244
|
+
alignment_context = ""
|
|
245
|
+
|
|
232
246
|
uname_sysname = os.uname().sysname
|
|
233
247
|
uname_machine = os.uname().machine
|
|
234
248
|
|
|
@@ -239,9 +253,11 @@ def initialize(
|
|
|
239
253
|
System: {uname_sysname}
|
|
240
254
|
Machine: {uname_machine}
|
|
241
255
|
Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
256
|
+
User home directory: {expanduser("~")}
|
|
242
257
|
|
|
243
258
|
{repo_context}
|
|
244
259
|
|
|
260
|
+
{alignment_context}
|
|
245
261
|
{initial_files_context}
|
|
246
262
|
|
|
247
263
|
---
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.1.1
|
|
4
4
|
Summary: Shell and coding agent on claude and chatgpt
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
@@ -11,6 +11,7 @@ Requires-Dist: fastapi>=0.115.0
|
|
|
11
11
|
Requires-Dist: openai>=1.46.0
|
|
12
12
|
Requires-Dist: petname>=2.6
|
|
13
13
|
Requires-Dist: pexpect>=4.9.0
|
|
14
|
+
Requires-Dist: psutil>=7.0.0
|
|
14
15
|
Requires-Dist: pydantic>=2.9.2
|
|
15
16
|
Requires-Dist: pygit2>=1.16.0
|
|
16
17
|
Requires-Dist: pyte>=0.8.2
|
|
@@ -29,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
29
30
|
|
|
30
31
|
Empowering chat applications to code, build and run on your local machine.
|
|
31
32
|
|
|
32
|
-
- Claude -
|
|
33
|
+
- Claude - MCP server with tightly integrated shell and code editing tools.
|
|
33
34
|
- Chatgpt - Allows custom gpt to talk to your shell via a relay server. (linux, mac, windows on wsl)
|
|
34
35
|
|
|
35
36
|
⚠️ Warning: do not allow BashCommand tool without reviewing the command, it may result in data loss.
|
|
@@ -46,6 +47,8 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
46
47
|
|
|
47
48
|
## Updates
|
|
48
49
|
|
|
50
|
+
- [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
|
|
51
|
+
|
|
49
52
|
- [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
|
|
50
53
|
|
|
51
54
|
- [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
|
|
@@ -59,7 +62,7 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
59
62
|
## 🚀 Highlights
|
|
60
63
|
|
|
61
64
|
- ⚡ **Create, Execute, Iterate**: Ask claude to keep running compiler checks till all errors are fixed, or ask it to keep checking for the status of a long running command till it's done.
|
|
62
|
-
- ⚡ **Large file edit**: Supports large file incremental edits to avoid token limit issues.
|
|
65
|
+
- ⚡ **Large file edit**: Supports large file incremental edits to avoid token limit issues. Smartly selects when to do small edits or large rewrite based on % of change needed.
|
|
63
66
|
- ⚡ **Syntax checking on edits**: Reports feedback to the LLM if its edits have any syntax errors, so that it can redo it.
|
|
64
67
|
- ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
|
|
65
68
|
- ⚡ **File protections**:
|
|
@@ -5,22 +5,24 @@ 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=HXYfGvhlDMxmiIX9AbB05wJcptJF_gSIobYhYSqWRJo,1685
|
|
7
7
|
wcgw/client/memory.py,sha256=M0plOGE5WXTEAs7nVLg4eCpVhmSW94ckpg5D0ycWX5I,2927
|
|
8
|
-
wcgw/client/modes.py,sha256=
|
|
9
|
-
wcgw/client/tool_prompts.py,sha256=
|
|
10
|
-
wcgw/client/tools.py,sha256=
|
|
11
|
-
wcgw/client/bash_state/bash_state.py,sha256=
|
|
8
|
+
wcgw/client/modes.py,sha256=gXm0u5EGQuPYEPZnyAptdzusN0JzLMED_DyErojPE6s,10679
|
|
9
|
+
wcgw/client/tool_prompts.py,sha256=zHJQHb_E44zA3rBIkAudibgP3Zbw0DjA-CKM8qlqmMU,4410
|
|
10
|
+
wcgw/client/tools.py,sha256=JXk6t7iNtP2EK6mdTkMqCquGGQ6TtoIJ8AEhkSith14,43963
|
|
11
|
+
wcgw/client/bash_state/bash_state.py,sha256=hbLBRcBhFCGqHKwxl9b3R1XXAvBLm0P7N7iYpDOh1CM,38190
|
|
12
|
+
wcgw/client/bash_state/parser/__init__.py,sha256=AnlNSmoQTSoqqlLOLX4P1uXfzc5VGeCGJsGgtisq2zE,207
|
|
13
|
+
wcgw/client/bash_state/parser/bash_statement_parser.py,sha256=9a8vPO1r3_tXmaAcubTQ5UY-NseWlalgm8LZA17LXuY,6058
|
|
12
14
|
wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
|
|
13
|
-
wcgw/client/file_ops/diff_edit.py,sha256=
|
|
14
|
-
wcgw/client/file_ops/search_replace.py,sha256=
|
|
15
|
+
wcgw/client/file_ops/diff_edit.py,sha256=eNxFRmVsBfY0ISwfsV5s8rlMwMxTlm6ko150iFFCsT8,18525
|
|
16
|
+
wcgw/client/file_ops/search_replace.py,sha256=TaIPDqjgmTo4oghhO3zIFklq5JjAbx_aPHJ7yEgvDh4,6854
|
|
15
17
|
wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
|
|
16
18
|
wcgw/client/mcp_server/__init__.py,sha256=mm7xhBIPwJpRT3u-Qsj4cKVMpVyucJoKRlbMP_gRRB0,343
|
|
17
|
-
wcgw/client/mcp_server/server.py,sha256=
|
|
19
|
+
wcgw/client/mcp_server/server.py,sha256=bHr9kv6hOs8s7LySNfpJeSX69GdFvFtbWpVyrp3G_eM,5160
|
|
18
20
|
wcgw/client/repo_ops/display_tree.py,sha256=uOGX2IbXTKXwtXT2wdDszuH4ODmSYsHm0toU55e1vYI,4021
|
|
19
21
|
wcgw/client/repo_ops/file_stats.py,sha256=AUA0Br7zFRpylWFYZPGMeGPJy3nWp9e2haKi34JptHE,4887
|
|
20
22
|
wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
|
|
21
23
|
wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
|
|
22
24
|
wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5rxTjoNnCos,1569052
|
|
23
|
-
wcgw/client/repo_ops/repo_context.py,sha256=
|
|
25
|
+
wcgw/client/repo_ops/repo_context.py,sha256=e_w-1VfxWQiZT3r66N13nlmPt6AGm0uvG3A7aYSgaCI,9632
|
|
24
26
|
wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
|
|
25
27
|
wcgw/relay/serve.py,sha256=vaHxSm4DkWUKLMOnz2cO6ClR2udnaXCWAGl0O_bXvrs,6984
|
|
26
28
|
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
@@ -52,8 +54,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
52
54
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
53
55
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
54
56
|
mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
|
|
55
|
-
wcgw-4.
|
|
56
|
-
wcgw-4.
|
|
57
|
-
wcgw-4.
|
|
58
|
-
wcgw-4.
|
|
59
|
-
wcgw-4.
|
|
57
|
+
wcgw-4.1.1.dist-info/METADATA,sha256=0uPUOufx9j1789XQHkl43hTwvJ5FD8QSX83bQRmI2Q8,14945
|
|
58
|
+
wcgw-4.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
59
|
+
wcgw-4.1.1.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
|
|
60
|
+
wcgw-4.1.1.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
61
|
+
wcgw-4.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|