wcgw 4.0.0__py3-none-any.whl → 4.1.0__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 +9 -7
- 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.0.dist-info}/METADATA +5 -3
- {wcgw-4.0.0.dist-info → wcgw-4.1.0.dist-info}/RECORD +13 -13
- {wcgw-4.0.0.dist-info → wcgw-4.1.0.dist-info}/WHEEL +0 -0
- {wcgw-4.0.0.dist-info → wcgw-4.1.0.dist-info}/entry_points.txt +0 -0
- {wcgw-4.0.0.dist-info → wcgw-4.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -125,7 +125,6 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
|
|
|
125
125
|
session_info = line.split()[0].strip() # e.g., "1234.my_screen"
|
|
126
126
|
if session_info.endswith(f".{name}"):
|
|
127
127
|
sessions_to_kill.append(session_info)
|
|
128
|
-
|
|
129
128
|
# Now, for every session we found, tell screen to quit it.
|
|
130
129
|
for session in sessions_to_kill:
|
|
131
130
|
try:
|
|
@@ -258,7 +257,12 @@ class BashState:
|
|
|
258
257
|
|
|
259
258
|
def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
|
|
260
259
|
self.close_bg_expect_thread()
|
|
261
|
-
|
|
260
|
+
try:
|
|
261
|
+
output = self._shell.expect(pattern, timeout)
|
|
262
|
+
except pexpect.TIMEOUT:
|
|
263
|
+
# Edge case: gets raised when the child fd is not ready in some timeout
|
|
264
|
+
# pexpect/utils.py:143
|
|
265
|
+
return 1
|
|
262
266
|
return output
|
|
263
267
|
|
|
264
268
|
def send(self, s: str | bytes, set_as_command: Optional[str]) -> int:
|
|
@@ -319,11 +323,9 @@ class BashState:
|
|
|
319
323
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
320
324
|
|
|
321
325
|
def cleanup(self) -> None:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
finally:
|
|
326
|
-
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
326
|
+
cleanup_all_screens_with_name(self._shell_id, self.console)
|
|
327
|
+
self.close_bg_expect_thread()
|
|
328
|
+
self._shell.close(True)
|
|
327
329
|
|
|
328
330
|
def __enter__(self) -> "BashState":
|
|
329
331
|
return self
|
|
@@ -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.0
|
|
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>
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
Empowering chat applications to code, build and run on your local machine.
|
|
31
31
|
|
|
32
|
-
- Claude -
|
|
32
|
+
- Claude - MCP server with tightly integrated shell and code editing tools.
|
|
33
33
|
- Chatgpt - Allows custom gpt to talk to your shell via a relay server. (linux, mac, windows on wsl)
|
|
34
34
|
|
|
35
35
|
⚠️ Warning: do not allow BashCommand tool without reviewing the command, it may result in data loss.
|
|
@@ -46,6 +46,8 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
46
46
|
|
|
47
47
|
## Updates
|
|
48
48
|
|
|
49
|
+
- [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
|
|
50
|
+
|
|
49
51
|
- [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
|
|
50
52
|
|
|
51
53
|
- [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
|
|
@@ -59,7 +61,7 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
59
61
|
## 🚀 Highlights
|
|
60
62
|
|
|
61
63
|
- ⚡ **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.
|
|
64
|
+
- ⚡ **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
65
|
- ⚡ **Syntax checking on edits**: Reports feedback to the LLM if its edits have any syntax errors, so that it can redo it.
|
|
64
66
|
- ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
|
|
65
67
|
- ⚡ **File protections**:
|
|
@@ -5,22 +5,22 @@ 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=1i72oLJ73AHvj0XmK0kAKnksiZwxWdAg21K27loKmpo,34020
|
|
12
12
|
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=
|
|
13
|
+
wcgw/client/file_ops/diff_edit.py,sha256=eNxFRmVsBfY0ISwfsV5s8rlMwMxTlm6ko150iFFCsT8,18525
|
|
14
|
+
wcgw/client/file_ops/search_replace.py,sha256=TaIPDqjgmTo4oghhO3zIFklq5JjAbx_aPHJ7yEgvDh4,6854
|
|
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=
|
|
17
|
+
wcgw/client/mcp_server/server.py,sha256=bHr9kv6hOs8s7LySNfpJeSX69GdFvFtbWpVyrp3G_eM,5160
|
|
18
18
|
wcgw/client/repo_ops/display_tree.py,sha256=uOGX2IbXTKXwtXT2wdDszuH4ODmSYsHm0toU55e1vYI,4021
|
|
19
19
|
wcgw/client/repo_ops/file_stats.py,sha256=AUA0Br7zFRpylWFYZPGMeGPJy3nWp9e2haKi34JptHE,4887
|
|
20
20
|
wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9tZ4M,2144
|
|
21
21
|
wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
|
|
22
22
|
wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5rxTjoNnCos,1569052
|
|
23
|
-
wcgw/client/repo_ops/repo_context.py,sha256=
|
|
23
|
+
wcgw/client/repo_ops/repo_context.py,sha256=e_w-1VfxWQiZT3r66N13nlmPt6AGm0uvG3A7aYSgaCI,9632
|
|
24
24
|
wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
|
|
25
25
|
wcgw/relay/serve.py,sha256=vaHxSm4DkWUKLMOnz2cO6ClR2udnaXCWAGl0O_bXvrs,6984
|
|
26
26
|
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
@@ -52,8 +52,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
52
52
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
53
53
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
54
54
|
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.
|
|
55
|
+
wcgw-4.1.0.dist-info/METADATA,sha256=6v32rPTVYTPeEGY9F0miFZRknGe31eeN0bZKWskoSW8,14916
|
|
56
|
+
wcgw-4.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
57
|
+
wcgw-4.1.0.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
|
|
58
|
+
wcgw-4.1.0.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
59
|
+
wcgw-4.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|