wcgw 5.1.2__py3-none-any.whl → 5.2.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 +39 -37
- wcgw/client/file_ops/search_replace.py +1 -1
- wcgw/client/tool_prompts.py +2 -4
- wcgw/client/tools.py +59 -48
- wcgw/types_.py +16 -7
- {wcgw-5.1.2.dist-info → wcgw-5.2.0.dist-info}/METADATA +27 -2
- {wcgw-5.1.2.dist-info → wcgw-5.2.0.dist-info}/RECORD +10 -10
- {wcgw-5.1.2.dist-info → wcgw-5.2.0.dist-info}/WHEEL +0 -0
- {wcgw-5.1.2.dist-info → wcgw-5.2.0.dist-info}/entry_points.txt +0 -0
- {wcgw-5.1.2.dist-info → wcgw-5.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -342,30 +342,30 @@ def get_bash_state_dir_xdg() -> str:
|
|
|
342
342
|
return bash_state_dir
|
|
343
343
|
|
|
344
344
|
|
|
345
|
-
def
|
|
346
|
-
"""Generate a random 4-digit
|
|
345
|
+
def generate_thread_id() -> str:
|
|
346
|
+
"""Generate a random 4-digit thread_id."""
|
|
347
347
|
return f"i{random.randint(1000, 9999)}"
|
|
348
348
|
|
|
349
349
|
|
|
350
|
-
def save_bash_state_by_id(
|
|
351
|
-
"""Save bash state to XDG directory with the given
|
|
352
|
-
if not
|
|
350
|
+
def save_bash_state_by_id(thread_id: str, bash_state_dict: dict[str, Any]) -> None:
|
|
351
|
+
"""Save bash state to XDG directory with the given thread_id."""
|
|
352
|
+
if not thread_id:
|
|
353
353
|
return
|
|
354
354
|
|
|
355
355
|
bash_state_dir = get_bash_state_dir_xdg()
|
|
356
|
-
state_file = os.path.join(bash_state_dir, f"{
|
|
356
|
+
state_file = os.path.join(bash_state_dir, f"{thread_id}_bash_state.json")
|
|
357
357
|
|
|
358
358
|
with open(state_file, "w") as f:
|
|
359
359
|
json.dump(bash_state_dict, f, indent=2)
|
|
360
360
|
|
|
361
361
|
|
|
362
|
-
def load_bash_state_by_id(
|
|
363
|
-
"""Load bash state from XDG directory with the given
|
|
364
|
-
if not
|
|
362
|
+
def load_bash_state_by_id(thread_id: str) -> Optional[dict[str, Any]]:
|
|
363
|
+
"""Load bash state from XDG directory with the given thread_id."""
|
|
364
|
+
if not thread_id:
|
|
365
365
|
return None
|
|
366
366
|
|
|
367
367
|
bash_state_dir = get_bash_state_dir_xdg()
|
|
368
|
-
state_file = os.path.join(bash_state_dir, f"{
|
|
368
|
+
state_file = os.path.join(bash_state_dir, f"{thread_id}_bash_state.json")
|
|
369
369
|
|
|
370
370
|
if not os.path.exists(state_file):
|
|
371
371
|
return None
|
|
@@ -376,7 +376,7 @@ def load_bash_state_by_id(chat_id: str) -> Optional[dict[str, Any]]:
|
|
|
376
376
|
|
|
377
377
|
class BashState:
|
|
378
378
|
_use_screen: bool
|
|
379
|
-
|
|
379
|
+
_current_thread_id: str
|
|
380
380
|
|
|
381
381
|
def __init__(
|
|
382
382
|
self,
|
|
@@ -388,7 +388,7 @@ class BashState:
|
|
|
388
388
|
mode: Optional[Modes],
|
|
389
389
|
use_screen: bool,
|
|
390
390
|
whitelist_for_overwrite: Optional[dict[str, "FileWhitelistData"]] = None,
|
|
391
|
-
|
|
391
|
+
thread_id: Optional[str] = None,
|
|
392
392
|
) -> None:
|
|
393
393
|
self._last_command: str = ""
|
|
394
394
|
self.console = console
|
|
@@ -406,8 +406,10 @@ class BashState:
|
|
|
406
406
|
self._whitelist_for_overwrite: dict[str, FileWhitelistData] = (
|
|
407
407
|
whitelist_for_overwrite or {}
|
|
408
408
|
)
|
|
409
|
-
# Always ensure we have a
|
|
410
|
-
self.
|
|
409
|
+
# Always ensure we have a thread_id
|
|
410
|
+
self._current_thread_id = (
|
|
411
|
+
thread_id if thread_id is not None else generate_thread_id()
|
|
412
|
+
)
|
|
411
413
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
412
414
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
413
415
|
self._use_screen = use_screen
|
|
@@ -632,22 +634,22 @@ class BashState:
|
|
|
632
634
|
self._init_shell()
|
|
633
635
|
|
|
634
636
|
@property
|
|
635
|
-
def
|
|
636
|
-
"""Get the current
|
|
637
|
-
return self.
|
|
637
|
+
def current_thread_id(self) -> str:
|
|
638
|
+
"""Get the current thread_id."""
|
|
639
|
+
return self._current_thread_id
|
|
638
640
|
|
|
639
|
-
def
|
|
641
|
+
def load_state_from_thread_id(self, thread_id: str) -> bool:
|
|
640
642
|
"""
|
|
641
|
-
Load bash state from a
|
|
643
|
+
Load bash state from a thread_id.
|
|
642
644
|
|
|
643
645
|
Args:
|
|
644
|
-
|
|
646
|
+
thread_id: The thread_id to load state from
|
|
645
647
|
|
|
646
648
|
Returns:
|
|
647
649
|
bool: True if state was successfully loaded, False otherwise
|
|
648
650
|
"""
|
|
649
651
|
# Try to load state from disk
|
|
650
|
-
loaded_state = load_bash_state_by_id(
|
|
652
|
+
loaded_state = load_bash_state_by_id(thread_id)
|
|
651
653
|
if not loaded_state:
|
|
652
654
|
return False
|
|
653
655
|
|
|
@@ -661,7 +663,7 @@ class BashState:
|
|
|
661
663
|
parsed_state[4],
|
|
662
664
|
parsed_state[5],
|
|
663
665
|
parsed_state[5],
|
|
664
|
-
|
|
666
|
+
thread_id,
|
|
665
667
|
)
|
|
666
668
|
return True
|
|
667
669
|
|
|
@@ -676,13 +678,13 @@ class BashState:
|
|
|
676
678
|
},
|
|
677
679
|
"mode": self._mode,
|
|
678
680
|
"workspace_root": self._workspace_root,
|
|
679
|
-
"chat_id": self.
|
|
681
|
+
"chat_id": self._current_thread_id,
|
|
680
682
|
}
|
|
681
683
|
|
|
682
684
|
def save_state_to_disk(self) -> None:
|
|
683
|
-
"""Save the current bash state to disk using the
|
|
685
|
+
"""Save the current bash state to disk using the thread_id."""
|
|
684
686
|
state_dict = self.serialize()
|
|
685
|
-
save_bash_state_by_id(self.
|
|
687
|
+
save_bash_state_by_id(self._current_thread_id, state_dict)
|
|
686
688
|
|
|
687
689
|
@staticmethod
|
|
688
690
|
def parse_state(
|
|
@@ -721,10 +723,10 @@ class BashState:
|
|
|
721
723
|
for k in whitelist_state
|
|
722
724
|
}
|
|
723
725
|
|
|
724
|
-
# Get the
|
|
725
|
-
|
|
726
|
-
if
|
|
727
|
-
|
|
726
|
+
# Get the thread_id from state, or generate a new one if not present
|
|
727
|
+
thread_id = state.get("chat_id")
|
|
728
|
+
if thread_id is None:
|
|
729
|
+
thread_id = generate_thread_id()
|
|
728
730
|
|
|
729
731
|
return (
|
|
730
732
|
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
@@ -733,7 +735,7 @@ class BashState:
|
|
|
733
735
|
state["mode"],
|
|
734
736
|
whitelist_dict,
|
|
735
737
|
state.get("workspace_root", ""),
|
|
736
|
-
|
|
738
|
+
thread_id,
|
|
737
739
|
)
|
|
738
740
|
|
|
739
741
|
def load_state(
|
|
@@ -745,7 +747,7 @@ class BashState:
|
|
|
745
747
|
whitelist_for_overwrite: dict[str, "FileWhitelistData"],
|
|
746
748
|
cwd: str,
|
|
747
749
|
workspace_root: str,
|
|
748
|
-
|
|
750
|
+
thread_id: str,
|
|
749
751
|
) -> None:
|
|
750
752
|
"""Create a new BashState instance from a serialized state dictionary"""
|
|
751
753
|
self._bash_command_mode = bash_command_mode
|
|
@@ -755,7 +757,7 @@ class BashState:
|
|
|
755
757
|
self._write_if_empty_mode = write_if_empty_mode
|
|
756
758
|
self._whitelist_for_overwrite = dict(whitelist_for_overwrite)
|
|
757
759
|
self._mode = mode
|
|
758
|
-
self.
|
|
760
|
+
self._current_thread_id = thread_id
|
|
759
761
|
self.reset_shell()
|
|
760
762
|
|
|
761
763
|
# Save state to disk after loading
|
|
@@ -1000,12 +1002,12 @@ def execute_bash(
|
|
|
1000
1002
|
timeout_s: Optional[float],
|
|
1001
1003
|
) -> tuple[str, float]:
|
|
1002
1004
|
try:
|
|
1003
|
-
# Check if the
|
|
1004
|
-
if bash_arg.
|
|
1005
|
-
# Try to load state from the
|
|
1006
|
-
if not bash_state.
|
|
1005
|
+
# Check if the thread_id matches current
|
|
1006
|
+
if bash_arg.thread_id != bash_state.current_thread_id:
|
|
1007
|
+
# Try to load state from the thread_id
|
|
1008
|
+
if not bash_state.load_state_from_thread_id(bash_arg.thread_id):
|
|
1007
1009
|
return (
|
|
1008
|
-
f"Error: No saved bash state found for
|
|
1010
|
+
f"Error: No saved bash state found for thread_id {bash_arg.thread_id}. Please initialize first with this ID.",
|
|
1009
1011
|
0.0,
|
|
1010
1012
|
)
|
|
1011
1013
|
|
|
@@ -4,7 +4,7 @@ from typing import Callable, Optional
|
|
|
4
4
|
from .diff_edit import FileEditInput, FileEditOutput, SearchReplaceMatchError
|
|
5
5
|
|
|
6
6
|
# Global regex patterns
|
|
7
|
-
SEARCH_MARKER = re.compile(r"^<<<<<<+\s*SEARCH
|
|
7
|
+
SEARCH_MARKER = re.compile(r"^<<<<<<+\s*SEARCH>?\s*$")
|
|
8
8
|
DIVIDER_MARKER = re.compile(r"^======*\s*$")
|
|
9
9
|
REPLACE_MARKER = re.compile(r"^>>>>>>+\s*REPLACE\s*$")
|
|
10
10
|
|
wcgw/client/tool_prompts.py
CHANGED
|
@@ -74,12 +74,10 @@ TOOL_PROMPTS = [
|
|
|
74
74
|
description="""
|
|
75
75
|
- Writes or edits a file based on the percentage of changes.
|
|
76
76
|
- Use absolute path only (~ allowed).
|
|
77
|
-
- percentage_to_change is calculated as number of existing lines that will have some diff divided by total existing lines.
|
|
78
77
|
- First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change
|
|
79
78
|
- 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.
|
|
80
|
-
- If percentage_to_change > 50, provide full file content in
|
|
81
|
-
- If percentage_to_change <= 50,
|
|
82
|
-
|
|
79
|
+
- If percentage_to_change > 50, provide full file content in text_or_search_replace_blocks
|
|
80
|
+
- If percentage_to_change <= 50, text_or_search_replace_blocks should be search/replace blocks.
|
|
83
81
|
"""
|
|
84
82
|
+ diffinstructions,
|
|
85
83
|
annotations=ToolAnnotations(
|
wcgw/client/tools.py
CHANGED
|
@@ -26,12 +26,13 @@ from openai.types.chat import (
|
|
|
26
26
|
ChatCompletionMessageParam,
|
|
27
27
|
)
|
|
28
28
|
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
29
|
-
from syntax_checker import
|
|
29
|
+
from syntax_checker import Output as SCOutput
|
|
30
|
+
from syntax_checker import check_syntax as raw_check_syntax
|
|
30
31
|
|
|
31
32
|
from ..client.bash_state.bash_state import (
|
|
32
33
|
BashState,
|
|
33
34
|
execute_bash,
|
|
34
|
-
|
|
35
|
+
generate_thread_id,
|
|
35
36
|
get_status,
|
|
36
37
|
)
|
|
37
38
|
from ..client.repo_ops.file_stats import (
|
|
@@ -78,6 +79,13 @@ class Context:
|
|
|
78
79
|
console: Console
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
def check_syntax(ext: str, content: str) -> SCOutput:
|
|
83
|
+
if ext == "html":
|
|
84
|
+
# Ignore due to prevelance of templating, causing issues
|
|
85
|
+
return raw_check_syntax("html", "")
|
|
86
|
+
return raw_check_syntax(ext, content)
|
|
87
|
+
|
|
88
|
+
|
|
81
89
|
def get_mode_prompt(context: Context) -> str:
|
|
82
90
|
mode_prompt = ""
|
|
83
91
|
if context.bash_state.mode == "code_writer":
|
|
@@ -103,7 +111,7 @@ def initialize(
|
|
|
103
111
|
coding_max_tokens: Optional[int],
|
|
104
112
|
noncoding_max_tokens: Optional[int],
|
|
105
113
|
mode: ModesConfig,
|
|
106
|
-
|
|
114
|
+
thread_id: str,
|
|
107
115
|
) -> tuple[str, Context, dict[str, list[tuple[int, int]]]]:
|
|
108
116
|
# Expand the workspace path
|
|
109
117
|
any_workspace_path = expand_user(any_workspace_path)
|
|
@@ -113,16 +121,16 @@ def initialize(
|
|
|
113
121
|
loaded_state = None
|
|
114
122
|
|
|
115
123
|
# For workspace/mode changes, ensure we're using an existing state if possible
|
|
116
|
-
if type != "first_call" and
|
|
117
|
-
# Try to load state from the
|
|
118
|
-
if not context.bash_state.
|
|
124
|
+
if type != "first_call" and thread_id != context.bash_state.current_thread_id:
|
|
125
|
+
# Try to load state from the thread_id
|
|
126
|
+
if not context.bash_state.load_state_from_thread_id(thread_id):
|
|
119
127
|
return (
|
|
120
|
-
f"Error: No saved bash state found for
|
|
128
|
+
f"Error: No saved bash state found for thread_id {thread_id}. Please re-initialize to get a new id or use correct id.",
|
|
121
129
|
context,
|
|
122
130
|
{},
|
|
123
131
|
)
|
|
124
132
|
del (
|
|
125
|
-
|
|
133
|
+
thread_id
|
|
126
134
|
) # No use other than loading correct state before doing actual tool related stuff
|
|
127
135
|
|
|
128
136
|
# Handle task resumption - this applies only to first_call
|
|
@@ -181,10 +189,10 @@ def initialize(
|
|
|
181
189
|
workspace_root = (
|
|
182
190
|
str(folder_to_start) if folder_to_start else parsed_state[5]
|
|
183
191
|
)
|
|
184
|
-
|
|
192
|
+
loaded_thread_id = parsed_state[6] if len(parsed_state) > 6 else None
|
|
185
193
|
|
|
186
|
-
if not
|
|
187
|
-
|
|
194
|
+
if not loaded_thread_id:
|
|
195
|
+
loaded_thread_id = context.bash_state.current_thread_id
|
|
188
196
|
|
|
189
197
|
if mode == "wcgw":
|
|
190
198
|
context.bash_state.load_state(
|
|
@@ -195,7 +203,7 @@ def initialize(
|
|
|
195
203
|
{**parsed_state[4], **context.bash_state.whitelist_for_overwrite},
|
|
196
204
|
str(folder_to_start) if folder_to_start else workspace_root,
|
|
197
205
|
workspace_root,
|
|
198
|
-
|
|
206
|
+
loaded_thread_id,
|
|
199
207
|
)
|
|
200
208
|
else:
|
|
201
209
|
state = modes_to_state(mode)
|
|
@@ -207,7 +215,7 @@ def initialize(
|
|
|
207
215
|
{**parsed_state[4], **context.bash_state.whitelist_for_overwrite},
|
|
208
216
|
str(folder_to_start) if folder_to_start else workspace_root,
|
|
209
217
|
workspace_root,
|
|
210
|
-
|
|
218
|
+
loaded_thread_id,
|
|
211
219
|
)
|
|
212
220
|
except ValueError:
|
|
213
221
|
context.console.print(traceback.format_exc())
|
|
@@ -217,10 +225,10 @@ def initialize(
|
|
|
217
225
|
else:
|
|
218
226
|
mode_changed = is_mode_change(mode, context.bash_state)
|
|
219
227
|
state = modes_to_state(mode)
|
|
220
|
-
|
|
228
|
+
new_thread_id = context.bash_state.current_thread_id
|
|
221
229
|
if type == "first_call":
|
|
222
|
-
# Recreate
|
|
223
|
-
|
|
230
|
+
# Recreate thread_id
|
|
231
|
+
new_thread_id = generate_thread_id()
|
|
224
232
|
# Use the provided workspace path as the workspace root
|
|
225
233
|
context.bash_state.load_state(
|
|
226
234
|
state[0],
|
|
@@ -230,7 +238,7 @@ def initialize(
|
|
|
230
238
|
dict(context.bash_state.whitelist_for_overwrite),
|
|
231
239
|
str(folder_to_start) if folder_to_start else "",
|
|
232
240
|
str(folder_to_start) if folder_to_start else "",
|
|
233
|
-
|
|
241
|
+
new_thread_id,
|
|
234
242
|
)
|
|
235
243
|
if type == "first_call" or mode_changed:
|
|
236
244
|
mode_prompt = get_mode_prompt(context)
|
|
@@ -273,6 +281,8 @@ def initialize(
|
|
|
273
281
|
uname_machine = os.uname().machine
|
|
274
282
|
|
|
275
283
|
output = f"""
|
|
284
|
+
Use thread_id={context.bash_state.current_thread_id} for all wcgw tool calls which take that.
|
|
285
|
+
---
|
|
276
286
|
{mode_prompt}
|
|
277
287
|
|
|
278
288
|
# Environment
|
|
@@ -281,17 +291,16 @@ Machine: {uname_machine}
|
|
|
281
291
|
Initialized in directory (also cwd): {context.bash_state.cwd}
|
|
282
292
|
User home directory: {expanduser("~")}
|
|
283
293
|
|
|
284
|
-
{repo_context}
|
|
285
|
-
|
|
286
294
|
{alignment_context}
|
|
287
|
-
{
|
|
295
|
+
{repo_context}
|
|
288
296
|
|
|
289
297
|
---
|
|
290
298
|
|
|
291
299
|
{memory}
|
|
292
|
-
|
|
293
300
|
---
|
|
294
|
-
|
|
301
|
+
|
|
302
|
+
{initial_files_context}
|
|
303
|
+
|
|
295
304
|
"""
|
|
296
305
|
|
|
297
306
|
return output, context, initial_paths_with_ranges
|
|
@@ -313,13 +322,13 @@ def reset_wcgw(
|
|
|
313
322
|
starting_directory: str,
|
|
314
323
|
mode_name: Optional[Modes],
|
|
315
324
|
change_mode: ModesConfig,
|
|
316
|
-
|
|
325
|
+
thread_id: str,
|
|
317
326
|
) -> str:
|
|
318
|
-
# Load state for this
|
|
319
|
-
if
|
|
320
|
-
# Try to load state from the
|
|
321
|
-
if not context.bash_state.
|
|
322
|
-
return f"Error: No saved bash state found for
|
|
327
|
+
# Load state for this thread_id before proceeding with mode/directory changes
|
|
328
|
+
if thread_id != context.bash_state.current_thread_id:
|
|
329
|
+
# Try to load state from the thread_id
|
|
330
|
+
if not context.bash_state.load_state_from_thread_id(thread_id):
|
|
331
|
+
return f"Error: No saved bash state found for thread_id {thread_id}. Please re-initialize to get a new id or use correct id."
|
|
323
332
|
if mode_name:
|
|
324
333
|
# update modes if they're relative
|
|
325
334
|
if isinstance(change_mode, CodeWriterMode):
|
|
@@ -332,7 +341,7 @@ def reset_wcgw(
|
|
|
332
341
|
change_mode
|
|
333
342
|
)
|
|
334
343
|
|
|
335
|
-
# Reset shell with new mode, using the provided
|
|
344
|
+
# Reset shell with new mode, using the provided thread_id
|
|
336
345
|
context.bash_state.load_state(
|
|
337
346
|
bash_command_mode,
|
|
338
347
|
file_edit_mode,
|
|
@@ -341,7 +350,7 @@ def reset_wcgw(
|
|
|
341
350
|
dict(context.bash_state.whitelist_for_overwrite),
|
|
342
351
|
starting_directory,
|
|
343
352
|
starting_directory,
|
|
344
|
-
|
|
353
|
+
thread_id,
|
|
345
354
|
)
|
|
346
355
|
mode_prompt = get_mode_prompt(context)
|
|
347
356
|
return (
|
|
@@ -357,7 +366,7 @@ def reset_wcgw(
|
|
|
357
366
|
write_if_empty_mode = context.bash_state.write_if_empty_mode
|
|
358
367
|
mode = context.bash_state.mode
|
|
359
368
|
|
|
360
|
-
# Reload state with new directory, using the provided
|
|
369
|
+
# Reload state with new directory, using the provided thread_id
|
|
361
370
|
context.bash_state.load_state(
|
|
362
371
|
bash_command_mode,
|
|
363
372
|
file_edit_mode,
|
|
@@ -366,7 +375,7 @@ def reset_wcgw(
|
|
|
366
375
|
dict(context.bash_state.whitelist_for_overwrite),
|
|
367
376
|
starting_directory,
|
|
368
377
|
starting_directory,
|
|
369
|
-
|
|
378
|
+
thread_id,
|
|
370
379
|
)
|
|
371
380
|
return "Reset successful" + get_status(context.bash_state)
|
|
372
381
|
|
|
@@ -508,7 +517,7 @@ def write_file(
|
|
|
508
517
|
error_on_exist_ = (
|
|
509
518
|
error_on_exist and path_ not in context.bash_state.whitelist_for_overwrite
|
|
510
519
|
)
|
|
511
|
-
|
|
520
|
+
curr_hash = ""
|
|
512
521
|
if error_on_exist and path_ in context.bash_state.whitelist_for_overwrite:
|
|
513
522
|
# Ensure hash has not changed
|
|
514
523
|
if os.path.exists(path_):
|
|
@@ -819,12 +828,14 @@ def file_writing(
|
|
|
819
828
|
If percentage_changed > 50%, treat content as direct file content.
|
|
820
829
|
Otherwise, treat content as search/replace blocks.
|
|
821
830
|
"""
|
|
822
|
-
# Check if the
|
|
823
|
-
if file_writing_args.
|
|
824
|
-
# Try to load state from the
|
|
825
|
-
if not context.bash_state.
|
|
831
|
+
# Check if the thread_id matches current
|
|
832
|
+
if file_writing_args.thread_id != context.bash_state.current_thread_id:
|
|
833
|
+
# Try to load state from the thread_id
|
|
834
|
+
if not context.bash_state.load_state_from_thread_id(
|
|
835
|
+
file_writing_args.thread_id
|
|
836
|
+
):
|
|
826
837
|
return (
|
|
827
|
-
f"Error: No saved bash state found for
|
|
838
|
+
f"Error: No saved bash state found for thread_id {file_writing_args.thread_id}. Please re-initialize to get a new id or use correct id.",
|
|
828
839
|
{},
|
|
829
840
|
)
|
|
830
841
|
|
|
@@ -837,14 +848,14 @@ def file_writing(
|
|
|
837
848
|
)
|
|
838
849
|
|
|
839
850
|
# If file doesn't exist, always use direct file_content mode
|
|
840
|
-
content = file_writing_args.
|
|
851
|
+
content = file_writing_args.text_or_search_replace_blocks
|
|
841
852
|
|
|
842
853
|
if not _is_edit(content, file_writing_args.percentage_to_change):
|
|
843
854
|
# Use direct content mode (same as WriteIfEmpty)
|
|
844
855
|
result, paths = write_file(
|
|
845
856
|
WriteIfEmpty(
|
|
846
857
|
file_path=path_,
|
|
847
|
-
file_content=file_writing_args.
|
|
858
|
+
file_content=file_writing_args.text_or_search_replace_blocks,
|
|
848
859
|
),
|
|
849
860
|
True,
|
|
850
861
|
coding_max_tokens,
|
|
@@ -857,7 +868,7 @@ def file_writing(
|
|
|
857
868
|
result, paths = do_diff_edit(
|
|
858
869
|
FileEdit(
|
|
859
870
|
file_path=path_,
|
|
860
|
-
file_edit_using_search_replace_blocks=file_writing_args.
|
|
871
|
+
file_edit_using_search_replace_blocks=file_writing_args.text_or_search_replace_blocks,
|
|
861
872
|
),
|
|
862
873
|
coding_max_tokens,
|
|
863
874
|
noncoding_max_tokens,
|
|
@@ -1012,7 +1023,7 @@ def get_tool_output(
|
|
|
1012
1023
|
)
|
|
1013
1024
|
workspace_path = workspace_path if os.path.exists(workspace_path) else ""
|
|
1014
1025
|
|
|
1015
|
-
# For these specific operations,
|
|
1026
|
+
# For these specific operations, thread_id is required
|
|
1016
1027
|
output = (
|
|
1017
1028
|
reset_wcgw(
|
|
1018
1029
|
context,
|
|
@@ -1021,7 +1032,7 @@ def get_tool_output(
|
|
|
1021
1032
|
if is_mode_change(arg.mode, context.bash_state)
|
|
1022
1033
|
else None,
|
|
1023
1034
|
arg.mode,
|
|
1024
|
-
arg.
|
|
1035
|
+
arg.thread_id,
|
|
1025
1036
|
),
|
|
1026
1037
|
0.0,
|
|
1027
1038
|
)
|
|
@@ -1035,7 +1046,7 @@ def get_tool_output(
|
|
|
1035
1046
|
coding_max_tokens,
|
|
1036
1047
|
noncoding_max_tokens,
|
|
1037
1048
|
arg.mode,
|
|
1038
|
-
arg.
|
|
1049
|
+
arg.thread_id,
|
|
1039
1050
|
)
|
|
1040
1051
|
output = output_, 0.0
|
|
1041
1052
|
# Since init_paths is already a dictionary mapping file paths to line ranges,
|
|
@@ -1315,7 +1326,7 @@ if __name__ == "__main__":
|
|
|
1315
1326
|
task_id_to_resume="",
|
|
1316
1327
|
mode_name="wcgw",
|
|
1317
1328
|
code_writer_config=None,
|
|
1318
|
-
|
|
1329
|
+
thread_id="",
|
|
1319
1330
|
),
|
|
1320
1331
|
default_enc,
|
|
1321
1332
|
0,
|
|
@@ -1329,7 +1340,7 @@ if __name__ == "__main__":
|
|
|
1329
1340
|
Context(BASH_STATE, BASH_STATE.console),
|
|
1330
1341
|
BashCommand(
|
|
1331
1342
|
action_json=Command(command="pwd"),
|
|
1332
|
-
|
|
1343
|
+
thread_id=BASH_STATE.current_thread_id,
|
|
1333
1344
|
),
|
|
1334
1345
|
default_enc,
|
|
1335
1346
|
0,
|
|
@@ -1359,9 +1370,9 @@ if __name__ == "__main__":
|
|
|
1359
1370
|
Context(BASH_STATE, BASH_STATE.console),
|
|
1360
1371
|
FileWriteOrEdit(
|
|
1361
1372
|
file_path="/Users/arusia/repos/wcgw/src/wcgw/client/tools.py",
|
|
1362
|
-
|
|
1373
|
+
text_or_search_replace_blocks="""test""",
|
|
1363
1374
|
percentage_to_change=100,
|
|
1364
|
-
|
|
1375
|
+
thread_id=BASH_STATE.current_thread_id,
|
|
1365
1376
|
),
|
|
1366
1377
|
default_enc,
|
|
1367
1378
|
0,
|
wcgw/types_.py
CHANGED
|
@@ -53,8 +53,8 @@ class Initialize(BaseModel):
|
|
|
53
53
|
initial_files_to_read: list[str]
|
|
54
54
|
task_id_to_resume: str
|
|
55
55
|
mode_name: Literal["wcgw", "architect", "code_writer"]
|
|
56
|
-
|
|
57
|
-
description="Use the
|
|
56
|
+
thread_id: str = Field(
|
|
57
|
+
description="Use the thread_id created in first_call, leave it as empty string if first_call"
|
|
58
58
|
)
|
|
59
59
|
code_writer_config: Optional[CodeWriterMode] = None
|
|
60
60
|
|
|
@@ -63,6 +63,10 @@ class Initialize(BaseModel):
|
|
|
63
63
|
assert self.code_writer_config is not None, (
|
|
64
64
|
"code_writer_config can't be null when the mode is code_writer"
|
|
65
65
|
)
|
|
66
|
+
if self.type != "first_call" and not self.thread_id:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
"Thread id should be provided if type != 'first_call', including when resetting"
|
|
69
|
+
)
|
|
66
70
|
return super().model_post_init(__context)
|
|
67
71
|
|
|
68
72
|
@property
|
|
@@ -105,7 +109,7 @@ class SendAscii(BaseModel):
|
|
|
105
109
|
class BashCommand(BaseModel):
|
|
106
110
|
action_json: Command | StatusCheck | SendText | SendSpecials | SendAscii
|
|
107
111
|
wait_for_seconds: Optional[float] = None
|
|
108
|
-
|
|
112
|
+
thread_id: str
|
|
109
113
|
|
|
110
114
|
|
|
111
115
|
class ReadImage(BaseModel):
|
|
@@ -215,10 +219,15 @@ class FileEdit(BaseModel):
|
|
|
215
219
|
|
|
216
220
|
|
|
217
221
|
class FileWriteOrEdit(BaseModel):
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
+
# Naming should be in sorted order otherwise it gets changed in LLM backend.
|
|
223
|
+
file_path: str = Field(description="#1: absolute file path")
|
|
224
|
+
percentage_to_change: int = Field(
|
|
225
|
+
description="#2: predict this percentage, calculated as number of existing lines that will have some diff divided by total existing lines."
|
|
226
|
+
)
|
|
227
|
+
text_or_search_replace_blocks: str = Field(
|
|
228
|
+
description="#3: content/edit blocks. Must be after #2 in the tool xml"
|
|
229
|
+
)
|
|
230
|
+
thread_id: str = Field(description="#4: thread_id")
|
|
222
231
|
|
|
223
232
|
|
|
224
233
|
class ContextSave(BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.2.0
|
|
4
4
|
Summary: Shell and coding agent for Claude and other mcp clients
|
|
5
5
|
Project-URL: Homepage, https://github.com/rusiaaman/wcgw
|
|
6
6
|
Author-email: Aman Rusia <gapypi@arcfu.com>
|
|
@@ -19,7 +19,7 @@ Requires-Dist: pyte>=0.8.2
|
|
|
19
19
|
Requires-Dist: python-dotenv>=1.0.1
|
|
20
20
|
Requires-Dist: rich>=13.8.1
|
|
21
21
|
Requires-Dist: semantic-version>=2.10.0
|
|
22
|
-
Requires-Dist: syntax-checker
|
|
22
|
+
Requires-Dist: syntax-checker==0.4.0b4
|
|
23
23
|
Requires-Dist: tokenizers>=0.21.0
|
|
24
24
|
Requires-Dist: toml>=0.10.2
|
|
25
25
|
Requires-Dist: tree-sitter-bash>=0.23.3
|
|
@@ -152,6 +152,31 @@ Then add or update the claude config file `%APPDATA%\Claude\claude_desktop_confi
|
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
```
|
|
155
|
+
When you encounter an error, execute the command wsl uv --python 3.12 wcgw@latest in command prompt. If you get the `error /bin/bash: line 1: uv: command not found`, it means uv was not installed globally and you need to point to the correct path of uv.
|
|
156
|
+
1. Find where uv is installed:
|
|
157
|
+
```bash
|
|
158
|
+
whereis uv
|
|
159
|
+
```
|
|
160
|
+
Example output:
|
|
161
|
+
```uv: /home/mywsl/.local/bin/uv```
|
|
162
|
+
|
|
163
|
+
2. Test the full path works:
|
|
164
|
+
```
|
|
165
|
+
wsl /home/mywsl/.local/bin/uv tool run --python 3.12 wcgw@latest
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
3. Update the config with the full path:
|
|
169
|
+
```
|
|
170
|
+
{
|
|
171
|
+
"mcpServers": {
|
|
172
|
+
"wcgw": {
|
|
173
|
+
"command": "wsl.exe",
|
|
174
|
+
"args": ["/home/mywsl/.local/bin/uv", "tool", "run", "--python", "3.12", "wcgw@latest"]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
Replace `/home/mywsl/.local/bin/uv` with your actual uv path from step 1.
|
|
155
180
|
|
|
156
181
|
### Usage
|
|
157
182
|
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
wcgw/__init__.py,sha256=JgAY25VsA208v8E7QTIU0E50nsk-TCJ4FWTEHmnssYU,127
|
|
2
2
|
wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
wcgw/types_.py,sha256=
|
|
3
|
+
wcgw/types_.py,sha256=nTj0Lr_ytBDHoeQ94ebO0mzuhsluBG6GmtT9eszIjuI,8141
|
|
4
4
|
wcgw/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
wcgw/client/common.py,sha256=OCH7Tx64jojz3M3iONUrGMadE07W21DiZs5sOxWX1Qc,1456
|
|
6
6
|
wcgw/client/diff-instructions.txt,sha256=eKRFA86yXWIGwNxIDaegTgTzIrFIBDWWiN1yP8Hf3i4,1685
|
|
7
7
|
wcgw/client/memory.py,sha256=U2Nw2si3Zg7n_RhNAuaYcmrrDtZ_Mooi-kfAOKflT-I,3079
|
|
8
8
|
wcgw/client/modes.py,sha256=roH6SPBokJMr5IzAlccdI-vJyvyS5vqSMMyth7TE86A,10315
|
|
9
|
-
wcgw/client/tool_prompts.py,sha256=
|
|
10
|
-
wcgw/client/tools.py,sha256=
|
|
11
|
-
wcgw/client/bash_state/bash_state.py,sha256=
|
|
9
|
+
wcgw/client/tool_prompts.py,sha256=0hfYmCzCo6kh_IC8RGJRfQ6hkB333GZvs_g3o_cvc2w,4662
|
|
10
|
+
wcgw/client/tools.py,sha256=Xo1lOLfzuDNZljESpkSOXlPUKG_4WJGAFaZ7K7ilV8s,48788
|
|
11
|
+
wcgw/client/bash_state/bash_state.py,sha256=b-Zo6pO3psfxu9TA0uw1ZZAa6bLmDPxbl7277fZfmrM,41862
|
|
12
12
|
wcgw/client/bash_state/parser/__init__.py,sha256=AnlNSmoQTSoqqlLOLX4P1uXfzc5VGeCGJsGgtisq2zE,207
|
|
13
13
|
wcgw/client/bash_state/parser/bash_statement_parser.py,sha256=9a8vPO1r3_tXmaAcubTQ5UY-NseWlalgm8LZA17LXuY,6058
|
|
14
14
|
wcgw/client/encoder/__init__.py,sha256=Y-8f43I6gMssUCWpX5rLYiAFv3D-JPRs4uNEejPlke8,1514
|
|
15
15
|
wcgw/client/file_ops/diff_edit.py,sha256=AwLq6-pY7czv1y-JA5O2Q4rgbvn82YmSL9jD8XB3Vo4,19019
|
|
16
16
|
wcgw/client/file_ops/extensions.py,sha256=CmfD7ON6SY24Prh2tRZdV9KbhuOrWqqk8qL1VtshzB8,3608
|
|
17
|
-
wcgw/client/file_ops/search_replace.py,sha256=
|
|
17
|
+
wcgw/client/file_ops/search_replace.py,sha256=5LFg-_U_ijnNrkYei4SWCPGKPGgDzJs49EDsIBzLmuY,6822
|
|
18
18
|
wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
|
|
19
19
|
wcgw/client/mcp_server/__init__.py,sha256=mm7xhBIPwJpRT3u-Qsj4cKVMpVyucJoKRlbMP_gRRB0,343
|
|
20
20
|
wcgw/client/mcp_server/server.py,sha256=jjwrmZZ8X0tXD0rsPZ9fKjEpdXpXCfdhEsN3Ho_tC8I,4989
|
|
@@ -30,8 +30,8 @@ wcgw_cli/anthropic_client.py,sha256=8bjDY59-aioyTJgpB-NBHZNhZaq6rqcTJcOf81kzCyA,
|
|
|
30
30
|
wcgw_cli/cli.py,sha256=-7FBe_lahKyUOhf65iurTA1M1gXXXAiT0OVKQVcZKKo,948
|
|
31
31
|
wcgw_cli/openai_client.py,sha256=GOqoSFazTV-cFjpdZGPM0DIwec8Up2TEcKUbsN40AGY,15990
|
|
32
32
|
wcgw_cli/openai_utils.py,sha256=xGOb3W5ALrIozV7oszfGYztpj0FnXdD7jAxm5lEIVKY,2439
|
|
33
|
-
wcgw-5.
|
|
34
|
-
wcgw-5.
|
|
35
|
-
wcgw-5.
|
|
36
|
-
wcgw-5.
|
|
37
|
-
wcgw-5.
|
|
33
|
+
wcgw-5.2.0.dist-info/METADATA,sha256=EW_fVplOiHFH1b8033vhslBodxghzY4Qw-03iugwuDg,15512
|
|
34
|
+
wcgw-5.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
35
|
+
wcgw-5.2.0.dist-info/entry_points.txt,sha256=UnjK-MAH4Qssh0tGJDMeij1oi-oRKokItkknP_BwShE,94
|
|
36
|
+
wcgw-5.2.0.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
37
|
+
wcgw-5.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|