wcgw 4.1.2__py3-none-any.whl → 5.0.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/__init__.py +3 -1
- wcgw/client/bash_state/bash_state.py +107 -0
- wcgw/client/file_ops/diff_edit.py +1 -0
- wcgw/client/modes.py +1 -8
- wcgw/client/tool_prompts.py +1 -1
- wcgw/client/tools.py +84 -42
- wcgw/types_.py +6 -1
- {wcgw-4.1.2.dist-info → wcgw-5.0.1.dist-info}/METADATA +12 -19
- {wcgw-4.1.2.dist-info → wcgw-5.0.1.dist-info}/RECORD +12 -15
- {wcgw-4.1.2.dist-info → wcgw-5.0.1.dist-info}/entry_points.txt +1 -2
- wcgw/relay/client.py +0 -95
- wcgw/relay/serve.py +0 -261
- wcgw/relay/static/privacy.txt +0 -7
- {wcgw-4.1.2.dist-info → wcgw-5.0.1.dist-info}/WHEEL +0 -0
- {wcgw-4.1.2.dist-info → wcgw-5.0.1.dist-info}/licenses/LICENSE +0 -0
wcgw/__init__.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import json
|
|
2
3
|
import os
|
|
3
4
|
import platform
|
|
5
|
+
import random
|
|
4
6
|
import subprocess
|
|
5
7
|
import threading
|
|
6
8
|
import time
|
|
@@ -332,8 +334,49 @@ P = ParamSpec("P")
|
|
|
332
334
|
R = TypeVar("R")
|
|
333
335
|
|
|
334
336
|
|
|
337
|
+
def get_bash_state_dir_xdg() -> str:
|
|
338
|
+
"""Get the XDG directory for storing bash state."""
|
|
339
|
+
xdg_data_dir = os.environ.get("XDG_DATA_HOME", os.path.expanduser("~/.local/share"))
|
|
340
|
+
bash_state_dir = os.path.join(xdg_data_dir, "wcgw", "bash_state")
|
|
341
|
+
os.makedirs(bash_state_dir, exist_ok=True)
|
|
342
|
+
return bash_state_dir
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def generate_chat_id() -> str:
|
|
346
|
+
"""Generate a random 4-digit chat ID."""
|
|
347
|
+
return f"i{random.randint(1000, 9999)}"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def save_bash_state_by_id(chat_id: str, bash_state_dict: dict[str, Any]) -> None:
|
|
351
|
+
"""Save bash state to XDG directory with the given chat ID."""
|
|
352
|
+
if not chat_id:
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
bash_state_dir = get_bash_state_dir_xdg()
|
|
356
|
+
state_file = os.path.join(bash_state_dir, f"{chat_id}_bash_state.json")
|
|
357
|
+
|
|
358
|
+
with open(state_file, "w") as f:
|
|
359
|
+
json.dump(bash_state_dict, f, indent=2)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def load_bash_state_by_id(chat_id: str) -> Optional[dict[str, Any]]:
|
|
363
|
+
"""Load bash state from XDG directory with the given chat ID."""
|
|
364
|
+
if not chat_id:
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
bash_state_dir = get_bash_state_dir_xdg()
|
|
368
|
+
state_file = os.path.join(bash_state_dir, f"{chat_id}_bash_state.json")
|
|
369
|
+
|
|
370
|
+
if not os.path.exists(state_file):
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
with open(state_file) as f:
|
|
374
|
+
return json.load(f) # type: ignore
|
|
375
|
+
|
|
376
|
+
|
|
335
377
|
class BashState:
|
|
336
378
|
_use_screen: bool
|
|
379
|
+
_current_chat_id: str
|
|
337
380
|
|
|
338
381
|
def __init__(
|
|
339
382
|
self,
|
|
@@ -345,6 +388,7 @@ class BashState:
|
|
|
345
388
|
mode: Optional[Modes],
|
|
346
389
|
use_screen: bool,
|
|
347
390
|
whitelist_for_overwrite: Optional[dict[str, "FileWhitelistData"]] = None,
|
|
391
|
+
chat_id: Optional[str] = None,
|
|
348
392
|
) -> None:
|
|
349
393
|
self._last_command: str = ""
|
|
350
394
|
self.console = console
|
|
@@ -362,6 +406,8 @@ class BashState:
|
|
|
362
406
|
self._whitelist_for_overwrite: dict[str, FileWhitelistData] = (
|
|
363
407
|
whitelist_for_overwrite or {}
|
|
364
408
|
)
|
|
409
|
+
# Always ensure we have a chat ID
|
|
410
|
+
self._current_chat_id = chat_id if chat_id is not None else generate_chat_id()
|
|
365
411
|
self._bg_expect_thread: Optional[threading.Thread] = None
|
|
366
412
|
self._bg_expect_thread_stop_event = threading.Event()
|
|
367
413
|
self._use_screen = use_screen
|
|
@@ -585,6 +631,40 @@ class BashState:
|
|
|
585
631
|
self.cleanup()
|
|
586
632
|
self._init_shell()
|
|
587
633
|
|
|
634
|
+
@property
|
|
635
|
+
def current_chat_id(self) -> str:
|
|
636
|
+
"""Get the current chat ID."""
|
|
637
|
+
return self._current_chat_id
|
|
638
|
+
|
|
639
|
+
def load_state_from_chat_id(self, chat_id: str) -> bool:
|
|
640
|
+
"""
|
|
641
|
+
Load bash state from a chat ID.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
chat_id: The chat ID to load state from
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
bool: True if state was successfully loaded, False otherwise
|
|
648
|
+
"""
|
|
649
|
+
# Try to load state from disk
|
|
650
|
+
loaded_state = load_bash_state_by_id(chat_id)
|
|
651
|
+
if not loaded_state:
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
# Parse and load the state
|
|
655
|
+
parsed_state = BashState.parse_state(loaded_state)
|
|
656
|
+
self.load_state(
|
|
657
|
+
parsed_state[0],
|
|
658
|
+
parsed_state[1],
|
|
659
|
+
parsed_state[2],
|
|
660
|
+
parsed_state[3],
|
|
661
|
+
parsed_state[4],
|
|
662
|
+
parsed_state[5],
|
|
663
|
+
parsed_state[5],
|
|
664
|
+
chat_id,
|
|
665
|
+
)
|
|
666
|
+
return True
|
|
667
|
+
|
|
588
668
|
def serialize(self) -> dict[str, Any]:
|
|
589
669
|
"""Serialize BashState to a dictionary for saving"""
|
|
590
670
|
return {
|
|
@@ -596,8 +676,14 @@ class BashState:
|
|
|
596
676
|
},
|
|
597
677
|
"mode": self._mode,
|
|
598
678
|
"workspace_root": self._workspace_root,
|
|
679
|
+
"chat_id": self._current_chat_id,
|
|
599
680
|
}
|
|
600
681
|
|
|
682
|
+
def save_state_to_disk(self) -> None:
|
|
683
|
+
"""Save the current bash state to disk using the chat ID."""
|
|
684
|
+
state_dict = self.serialize()
|
|
685
|
+
save_bash_state_by_id(self._current_chat_id, state_dict)
|
|
686
|
+
|
|
601
687
|
@staticmethod
|
|
602
688
|
def parse_state(
|
|
603
689
|
state: dict[str, Any],
|
|
@@ -608,6 +694,7 @@ class BashState:
|
|
|
608
694
|
Modes,
|
|
609
695
|
dict[str, "FileWhitelistData"],
|
|
610
696
|
str,
|
|
697
|
+
str,
|
|
611
698
|
]:
|
|
612
699
|
whitelist_state = state["whitelist_for_overwrite"]
|
|
613
700
|
# Convert serialized whitelist data back to FileWhitelistData objects
|
|
@@ -634,6 +721,11 @@ class BashState:
|
|
|
634
721
|
for k in whitelist_state
|
|
635
722
|
}
|
|
636
723
|
|
|
724
|
+
# Get the chat_id from state, or generate a new one if not present
|
|
725
|
+
chat_id = state.get("chat_id")
|
|
726
|
+
if chat_id is None:
|
|
727
|
+
chat_id = generate_chat_id()
|
|
728
|
+
|
|
637
729
|
return (
|
|
638
730
|
BashCommandMode.deserialize(state["bash_command_mode"]),
|
|
639
731
|
FileEditMode.deserialize(state["file_edit_mode"]),
|
|
@@ -641,6 +733,7 @@ class BashState:
|
|
|
641
733
|
state["mode"],
|
|
642
734
|
whitelist_dict,
|
|
643
735
|
state.get("workspace_root", ""),
|
|
736
|
+
chat_id,
|
|
644
737
|
)
|
|
645
738
|
|
|
646
739
|
def load_state(
|
|
@@ -652,6 +745,7 @@ class BashState:
|
|
|
652
745
|
whitelist_for_overwrite: dict[str, "FileWhitelistData"],
|
|
653
746
|
cwd: str,
|
|
654
747
|
workspace_root: str,
|
|
748
|
+
chat_id: str,
|
|
655
749
|
) -> None:
|
|
656
750
|
"""Create a new BashState instance from a serialized state dictionary"""
|
|
657
751
|
self._bash_command_mode = bash_command_mode
|
|
@@ -661,8 +755,12 @@ class BashState:
|
|
|
661
755
|
self._write_if_empty_mode = write_if_empty_mode
|
|
662
756
|
self._whitelist_for_overwrite = dict(whitelist_for_overwrite)
|
|
663
757
|
self._mode = mode
|
|
758
|
+
self._current_chat_id = chat_id
|
|
664
759
|
self.reset_shell()
|
|
665
760
|
|
|
761
|
+
# Save state to disk after loading
|
|
762
|
+
self.save_state_to_disk()
|
|
763
|
+
|
|
666
764
|
def get_pending_for(self) -> str:
|
|
667
765
|
if isinstance(self._state, datetime.datetime):
|
|
668
766
|
timedelta = datetime.datetime.now() - self._state
|
|
@@ -902,6 +1000,15 @@ def execute_bash(
|
|
|
902
1000
|
timeout_s: Optional[float],
|
|
903
1001
|
) -> tuple[str, float]:
|
|
904
1002
|
try:
|
|
1003
|
+
# Check if the chat ID matches current
|
|
1004
|
+
if bash_arg.chat_id != bash_state.current_chat_id:
|
|
1005
|
+
# Try to load state from the chat ID
|
|
1006
|
+
if not bash_state.load_state_from_chat_id(bash_arg.chat_id):
|
|
1007
|
+
return (
|
|
1008
|
+
f"Error: No saved bash state found for chat ID {bash_arg.chat_id}. Please initialize first with this ID.",
|
|
1009
|
+
0.0,
|
|
1010
|
+
)
|
|
1011
|
+
|
|
905
1012
|
output, cost = _execute_bash(bash_state, enc, bash_arg, max_tokens, timeout_s)
|
|
906
1013
|
|
|
907
1014
|
# Remove echo if it's a command
|
|
@@ -11,6 +11,7 @@ class SearchReplaceMatchError(Exception):
|
|
|
11
11
|
message = f"""
|
|
12
12
|
{message}
|
|
13
13
|
---
|
|
14
|
+
Edit failed, no changes are applied. You'll have to reapply all search/replace blocks again.
|
|
14
15
|
Retry immediately with same "percentage_to_change" using search replace blocks fixing above error.
|
|
15
16
|
"""
|
|
16
17
|
super().__init__(message)
|
wcgw/client/modes.py
CHANGED
|
@@ -89,7 +89,6 @@ You are now running in "code_writer" mode.
|
|
|
89
89
|
- Do not use echo to write multi-line files, always use FileWriteOrEdit tool to update a code.
|
|
90
90
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
91
91
|
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
92
|
-
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
93
92
|
- 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.
|
|
94
93
|
"""
|
|
95
94
|
|
|
@@ -114,13 +113,9 @@ You are now running in "code_writer" mode.
|
|
|
114
113
|
|
|
115
114
|
|
|
116
115
|
WCGW_PROMPT = """
|
|
117
|
-
|
|
118
|
-
You're an expert software engineer with shell and code knowledge.
|
|
119
|
-
|
|
120
|
-
Instructions:
|
|
116
|
+
# Instructions
|
|
121
117
|
|
|
122
118
|
- You should use the provided bash execution, reading and writing file tools to complete objective.
|
|
123
|
-
- First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
|
|
124
119
|
- Do not provide code snippets unless asked by the user, instead directly add/edit the code.
|
|
125
120
|
- Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
|
|
126
121
|
- 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
|
|
@@ -131,8 +126,6 @@ Instructions:
|
|
|
131
126
|
Additional instructions:
|
|
132
127
|
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.
|
|
133
128
|
|
|
134
|
-
Always write production ready, syntactically correct code.
|
|
135
|
-
|
|
136
129
|
|
|
137
130
|
"""
|
|
138
131
|
ARCHITECT_PROMPT = """
|
wcgw/client/tool_prompts.py
CHANGED
|
@@ -91,7 +91,7 @@ TOOL_PROMPTS = [
|
|
|
91
91
|
name="ContextSave",
|
|
92
92
|
description="""
|
|
93
93
|
Saves provided description and file contents of all the relevant file paths or globs in a single text file.
|
|
94
|
-
- Provide random unqiue id or whatever user provided.
|
|
94
|
+
- Provide random 3 word unqiue id or whatever user provided.
|
|
95
95
|
- Leave project path as empty string if no project path""",
|
|
96
96
|
),
|
|
97
97
|
]
|
wcgw/client/tools.py
CHANGED
|
@@ -28,13 +28,17 @@ from openai.types.chat import (
|
|
|
28
28
|
from pydantic import BaseModel, TypeAdapter, ValidationError
|
|
29
29
|
from syntax_checker import check_syntax
|
|
30
30
|
|
|
31
|
-
from
|
|
32
|
-
|
|
31
|
+
from ..client.bash_state.bash_state import (
|
|
32
|
+
BashState,
|
|
33
|
+
execute_bash,
|
|
34
|
+
generate_chat_id,
|
|
35
|
+
get_status,
|
|
36
|
+
)
|
|
37
|
+
from ..client.repo_ops.file_stats import (
|
|
33
38
|
FileStats,
|
|
34
39
|
load_workspace_stats,
|
|
35
40
|
save_workspace_stats,
|
|
36
41
|
)
|
|
37
|
-
|
|
38
42
|
from ..types_ import (
|
|
39
43
|
BashCommand,
|
|
40
44
|
CodeWriterMode,
|
|
@@ -50,10 +54,6 @@ from ..types_ import (
|
|
|
50
54
|
ReadImage,
|
|
51
55
|
WriteIfEmpty,
|
|
52
56
|
)
|
|
53
|
-
from .bash_state.bash_state import (
|
|
54
|
-
BashState,
|
|
55
|
-
execute_bash,
|
|
56
|
-
)
|
|
57
57
|
from .encoder import EncoderDecoder, get_default_encoder
|
|
58
58
|
from .file_ops.search_replace import (
|
|
59
59
|
DIVIDER_MARKER,
|
|
@@ -77,9 +77,6 @@ class Context:
|
|
|
77
77
|
console: Console
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
INITIALIZED = False
|
|
81
|
-
|
|
82
|
-
|
|
83
80
|
def get_mode_prompt(context: Context) -> str:
|
|
84
81
|
mode_prompt = ""
|
|
85
82
|
if context.bash_state.mode == "code_writer":
|
|
@@ -104,6 +101,7 @@ def initialize(
|
|
|
104
101
|
task_id_to_resume: str,
|
|
105
102
|
max_tokens: Optional[int],
|
|
106
103
|
mode: ModesConfig,
|
|
104
|
+
chat_id: str,
|
|
107
105
|
) -> tuple[str, Context, dict[str, list[tuple[int, int]]]]:
|
|
108
106
|
# Expand the workspace path
|
|
109
107
|
any_workspace_path = expand_user(any_workspace_path)
|
|
@@ -112,21 +110,34 @@ def initialize(
|
|
|
112
110
|
memory = ""
|
|
113
111
|
loaded_state = None
|
|
114
112
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
113
|
+
# For workspace/mode changes, ensure we're using an existing state if possible
|
|
114
|
+
if type != "first_call" and chat_id != context.bash_state.current_chat_id:
|
|
115
|
+
# Try to load state from the chat ID
|
|
116
|
+
if not context.bash_state.load_state_from_chat_id(chat_id):
|
|
117
|
+
return (
|
|
118
|
+
f"Error: No saved bash state found for chat ID {chat_id}",
|
|
119
|
+
context,
|
|
120
|
+
{},
|
|
121
|
+
)
|
|
122
|
+
del (
|
|
123
|
+
chat_id
|
|
124
|
+
) # No use other than loading correct state before doing actual tool related stuff
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
# Handle task resumption - this applies only to first_call
|
|
127
|
+
if type == "first_call" and task_id_to_resume:
|
|
128
|
+
try:
|
|
129
|
+
project_root_path, task_mem, loaded_state = load_memory(
|
|
130
|
+
task_id_to_resume,
|
|
131
|
+
max_tokens,
|
|
132
|
+
lambda x: default_enc.encoder(x),
|
|
133
|
+
lambda x: default_enc.decoder(x),
|
|
134
|
+
)
|
|
135
|
+
memory = "Following is the retrieved task:\n" + task_mem
|
|
136
|
+
if os.path.exists(project_root_path):
|
|
137
|
+
any_workspace_path = project_root_path
|
|
138
|
+
|
|
139
|
+
except Exception:
|
|
140
|
+
memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
|
|
130
141
|
elif task_id_to_resume:
|
|
131
142
|
memory = (
|
|
132
143
|
"Warning: task can only be resumed in a new conversation. No task loaded."
|
|
@@ -167,6 +178,11 @@ def initialize(
|
|
|
167
178
|
workspace_root = (
|
|
168
179
|
str(folder_to_start) if folder_to_start else parsed_state[5]
|
|
169
180
|
)
|
|
181
|
+
loaded_chat_id = parsed_state[6] if len(parsed_state) > 6 else None
|
|
182
|
+
|
|
183
|
+
if not loaded_chat_id:
|
|
184
|
+
loaded_chat_id = context.bash_state.current_chat_id
|
|
185
|
+
|
|
170
186
|
if mode == "wcgw":
|
|
171
187
|
context.bash_state.load_state(
|
|
172
188
|
parsed_state[0],
|
|
@@ -176,6 +192,7 @@ def initialize(
|
|
|
176
192
|
{**parsed_state[4], **context.bash_state.whitelist_for_overwrite},
|
|
177
193
|
str(folder_to_start) if folder_to_start else workspace_root,
|
|
178
194
|
workspace_root,
|
|
195
|
+
loaded_chat_id,
|
|
179
196
|
)
|
|
180
197
|
else:
|
|
181
198
|
state = modes_to_state(mode)
|
|
@@ -187,6 +204,7 @@ def initialize(
|
|
|
187
204
|
{**parsed_state[4], **context.bash_state.whitelist_for_overwrite},
|
|
188
205
|
str(folder_to_start) if folder_to_start else workspace_root,
|
|
189
206
|
workspace_root,
|
|
207
|
+
loaded_chat_id,
|
|
190
208
|
)
|
|
191
209
|
except ValueError:
|
|
192
210
|
context.console.print(traceback.format_exc())
|
|
@@ -196,6 +214,10 @@ def initialize(
|
|
|
196
214
|
else:
|
|
197
215
|
mode_changed = is_mode_change(mode, context.bash_state)
|
|
198
216
|
state = modes_to_state(mode)
|
|
217
|
+
new_chat_id = context.bash_state.current_chat_id
|
|
218
|
+
if type == "first_call":
|
|
219
|
+
# Recreate chat id
|
|
220
|
+
new_chat_id = generate_chat_id()
|
|
199
221
|
# Use the provided workspace path as the workspace root
|
|
200
222
|
context.bash_state.load_state(
|
|
201
223
|
state[0],
|
|
@@ -205,6 +227,7 @@ def initialize(
|
|
|
205
227
|
dict(context.bash_state.whitelist_for_overwrite),
|
|
206
228
|
str(folder_to_start) if folder_to_start else "",
|
|
207
229
|
str(folder_to_start) if folder_to_start else "",
|
|
230
|
+
new_chat_id,
|
|
208
231
|
)
|
|
209
232
|
if type == "first_call" or mode_changed:
|
|
210
233
|
mode_prompt = get_mode_prompt(context)
|
|
@@ -263,10 +286,11 @@ User home directory: {expanduser("~")}
|
|
|
263
286
|
---
|
|
264
287
|
|
|
265
288
|
{memory}
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
Use chat_id={context.bash_state.current_chat_id} for all wcgw tool calls which take that.
|
|
266
292
|
"""
|
|
267
293
|
|
|
268
|
-
global INITIALIZED
|
|
269
|
-
INITIALIZED = True
|
|
270
294
|
return output, context, initial_paths_with_ranges
|
|
271
295
|
|
|
272
296
|
|
|
@@ -286,8 +310,13 @@ def reset_wcgw(
|
|
|
286
310
|
starting_directory: str,
|
|
287
311
|
mode_name: Optional[Modes],
|
|
288
312
|
change_mode: ModesConfig,
|
|
313
|
+
chat_id: str,
|
|
289
314
|
) -> str:
|
|
290
|
-
|
|
315
|
+
# Load state for this chat_id before proceeding with mode/directory changes
|
|
316
|
+
if chat_id != context.bash_state.current_chat_id:
|
|
317
|
+
# Try to load state from the chat ID
|
|
318
|
+
if not context.bash_state.load_state_from_chat_id(chat_id):
|
|
319
|
+
return f"Error: No saved bash state found for chat ID {chat_id}"
|
|
291
320
|
if mode_name:
|
|
292
321
|
# update modes if they're relative
|
|
293
322
|
if isinstance(change_mode, CodeWriterMode):
|
|
@@ -300,7 +329,7 @@ def reset_wcgw(
|
|
|
300
329
|
change_mode
|
|
301
330
|
)
|
|
302
331
|
|
|
303
|
-
# Reset shell with new mode
|
|
332
|
+
# Reset shell with new mode, using the provided chat ID
|
|
304
333
|
context.bash_state.load_state(
|
|
305
334
|
bash_command_mode,
|
|
306
335
|
file_edit_mode,
|
|
@@ -309,9 +338,9 @@ def reset_wcgw(
|
|
|
309
338
|
dict(context.bash_state.whitelist_for_overwrite),
|
|
310
339
|
starting_directory,
|
|
311
340
|
starting_directory,
|
|
341
|
+
chat_id,
|
|
312
342
|
)
|
|
313
343
|
mode_prompt = get_mode_prompt(context)
|
|
314
|
-
INITIALIZED = True
|
|
315
344
|
return (
|
|
316
345
|
f"Reset successful with mode change to {mode_name}.\n"
|
|
317
346
|
+ mode_prompt
|
|
@@ -325,7 +354,7 @@ def reset_wcgw(
|
|
|
325
354
|
write_if_empty_mode = context.bash_state.write_if_empty_mode
|
|
326
355
|
mode = context.bash_state.mode
|
|
327
356
|
|
|
328
|
-
# Reload state with new directory
|
|
357
|
+
# Reload state with new directory, using the provided chat ID
|
|
329
358
|
context.bash_state.load_state(
|
|
330
359
|
bash_command_mode,
|
|
331
360
|
file_edit_mode,
|
|
@@ -334,8 +363,8 @@ def reset_wcgw(
|
|
|
334
363
|
dict(context.bash_state.whitelist_for_overwrite),
|
|
335
364
|
starting_directory,
|
|
336
365
|
starting_directory,
|
|
366
|
+
chat_id,
|
|
337
367
|
)
|
|
338
|
-
INITIALIZED = True
|
|
339
368
|
return "Reset successful" + get_status(context.bash_state)
|
|
340
369
|
|
|
341
370
|
|
|
@@ -763,6 +792,15 @@ def file_writing(
|
|
|
763
792
|
If percentage_changed > 50%, treat content as direct file content.
|
|
764
793
|
Otherwise, treat content as search/replace blocks.
|
|
765
794
|
"""
|
|
795
|
+
# Check if the chat ID matches current
|
|
796
|
+
if file_writing_args.chat_id != context.bash_state.current_chat_id:
|
|
797
|
+
# Try to load state from the chat ID
|
|
798
|
+
if not context.bash_state.load_state_from_chat_id(file_writing_args.chat_id):
|
|
799
|
+
return (
|
|
800
|
+
f"Error: No saved bash state found for chat ID {file_writing_args.chat_id}. Please initialize first with this ID.",
|
|
801
|
+
{},
|
|
802
|
+
)
|
|
803
|
+
|
|
766
804
|
# Expand the path before checking if it's absolute
|
|
767
805
|
path_ = expand_user(file_writing_args.file_path)
|
|
768
806
|
if not os.path.isabs(path_):
|
|
@@ -852,7 +890,7 @@ def get_tool_output(
|
|
|
852
890
|
loop_call: Callable[[str, float], tuple[str, float]],
|
|
853
891
|
max_tokens: Optional[int],
|
|
854
892
|
) -> tuple[list[str | ImageData], float]:
|
|
855
|
-
global TOOL_CALLS
|
|
893
|
+
global TOOL_CALLS
|
|
856
894
|
if isinstance(args, dict):
|
|
857
895
|
adapter = TypeAdapter[TOOLS](TOOLS, config={"extra": "forbid"})
|
|
858
896
|
arg = adapter.validate_python(args)
|
|
@@ -866,8 +904,6 @@ def get_tool_output(
|
|
|
866
904
|
|
|
867
905
|
if isinstance(arg, BashCommand):
|
|
868
906
|
context.console.print("Calling execute bash tool")
|
|
869
|
-
if not INITIALIZED:
|
|
870
|
-
raise Exception("Initialize tool not called yet.")
|
|
871
907
|
|
|
872
908
|
output_str, cost = execute_bash(
|
|
873
909
|
context.bash_state, enc, arg, max_tokens, arg.wait_for_seconds
|
|
@@ -875,8 +911,6 @@ def get_tool_output(
|
|
|
875
911
|
output = output_str, cost
|
|
876
912
|
elif isinstance(arg, WriteIfEmpty):
|
|
877
913
|
context.console.print("Calling write file tool")
|
|
878
|
-
if not INITIALIZED:
|
|
879
|
-
raise Exception("Initialize tool not called yet.")
|
|
880
914
|
|
|
881
915
|
result, write_paths = write_file(arg, True, max_tokens, context)
|
|
882
916
|
output = result, 0
|
|
@@ -888,8 +922,6 @@ def get_tool_output(
|
|
|
888
922
|
file_paths_with_ranges[path] = ranges.copy()
|
|
889
923
|
elif isinstance(arg, FileEdit):
|
|
890
924
|
context.console.print("Calling full file edit tool")
|
|
891
|
-
if not INITIALIZED:
|
|
892
|
-
raise Exception("Initialize tool not called yet.")
|
|
893
925
|
|
|
894
926
|
result, edit_paths = do_diff_edit(arg, max_tokens, context)
|
|
895
927
|
output = result, 0.0
|
|
@@ -901,8 +933,6 @@ def get_tool_output(
|
|
|
901
933
|
file_paths_with_ranges[path] = ranges.copy()
|
|
902
934
|
elif isinstance(arg, FileWriteOrEdit):
|
|
903
935
|
context.console.print("Calling file writing tool")
|
|
904
|
-
if not INITIALIZED:
|
|
905
|
-
raise Exception("Initialize tool not called yet.")
|
|
906
936
|
|
|
907
937
|
result, write_edit_paths = file_writing(arg, max_tokens, context)
|
|
908
938
|
output = result, 0.0
|
|
@@ -944,6 +974,8 @@ def get_tool_output(
|
|
|
944
974
|
else os.path.dirname(arg.any_workspace_path)
|
|
945
975
|
)
|
|
946
976
|
workspace_path = workspace_path if os.path.exists(workspace_path) else ""
|
|
977
|
+
|
|
978
|
+
# For these specific operations, chat_id is required
|
|
947
979
|
output = (
|
|
948
980
|
reset_wcgw(
|
|
949
981
|
context,
|
|
@@ -952,6 +984,7 @@ def get_tool_output(
|
|
|
952
984
|
if is_mode_change(arg.mode, context.bash_state)
|
|
953
985
|
else None,
|
|
954
986
|
arg.mode,
|
|
987
|
+
arg.chat_id,
|
|
955
988
|
),
|
|
956
989
|
0.0,
|
|
957
990
|
)
|
|
@@ -964,6 +997,7 @@ def get_tool_output(
|
|
|
964
997
|
arg.task_id_to_resume,
|
|
965
998
|
max_tokens,
|
|
966
999
|
arg.mode,
|
|
1000
|
+
arg.chat_id,
|
|
967
1001
|
)
|
|
968
1002
|
output = output_, 0.0
|
|
969
1003
|
# Since init_paths is already a dictionary mapping file paths to line ranges,
|
|
@@ -1009,6 +1043,9 @@ def get_tool_output(
|
|
|
1009
1043
|
if file_paths_with_ranges: # Only add to whitelist if we have paths
|
|
1010
1044
|
context.bash_state.add_to_whitelist_for_overwrite(file_paths_with_ranges)
|
|
1011
1045
|
|
|
1046
|
+
# Save bash_state
|
|
1047
|
+
context.bash_state.save_state_to_disk()
|
|
1048
|
+
|
|
1012
1049
|
if isinstance(output[0], str):
|
|
1013
1050
|
context.console.print(str(output[0]))
|
|
1014
1051
|
else:
|
|
@@ -1128,7 +1165,7 @@ def read_file(
|
|
|
1128
1165
|
with path.open("r") as f:
|
|
1129
1166
|
all_lines = f.readlines(10_000_000)
|
|
1130
1167
|
|
|
1131
|
-
if all_lines[-1].endswith("\n"):
|
|
1168
|
+
if all_lines and all_lines[-1].endswith("\n"):
|
|
1132
1169
|
# Special handling of line counts because readlines doesn't consider last empty line as a separate line
|
|
1133
1170
|
all_lines[-1] = all_lines[-1][:-1]
|
|
1134
1171
|
all_lines.append("")
|
|
@@ -1220,6 +1257,7 @@ if __name__ == "__main__":
|
|
|
1220
1257
|
task_id_to_resume="",
|
|
1221
1258
|
mode_name="wcgw",
|
|
1222
1259
|
code_writer_config=None,
|
|
1260
|
+
chat_id="",
|
|
1223
1261
|
),
|
|
1224
1262
|
default_enc,
|
|
1225
1263
|
0,
|
|
@@ -1230,7 +1268,10 @@ if __name__ == "__main__":
|
|
|
1230
1268
|
print(
|
|
1231
1269
|
get_tool_output(
|
|
1232
1270
|
Context(BASH_STATE, BASH_STATE.console),
|
|
1233
|
-
BashCommand(
|
|
1271
|
+
BashCommand(
|
|
1272
|
+
action_json=Command(command="pwd"),
|
|
1273
|
+
chat_id=BASH_STATE.current_chat_id,
|
|
1274
|
+
),
|
|
1234
1275
|
default_enc,
|
|
1235
1276
|
0,
|
|
1236
1277
|
lambda x, y: ("", 0),
|
|
@@ -1259,6 +1300,7 @@ if __name__ == "__main__":
|
|
|
1259
1300
|
file_path="/Users/arusia/repos/wcgw/src/wcgw/client/tools.py",
|
|
1260
1301
|
file_content_or_search_replace_blocks="""test""",
|
|
1261
1302
|
percentage_to_change=100,
|
|
1303
|
+
chat_id=BASH_STATE.current_chat_id,
|
|
1262
1304
|
),
|
|
1263
1305
|
default_enc,
|
|
1264
1306
|
0,
|
wcgw/types_.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
from typing import Any, List, Literal, Optional, Protocol, Sequence, Union
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel as PydanticBaseModel
|
|
5
|
-
from pydantic import PrivateAttr
|
|
5
|
+
from pydantic import Field, PrivateAttr
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class NoExtraArgs(PydanticBaseModel):
|
|
@@ -53,6 +53,9 @@ 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
|
+
chat_id: str = Field(
|
|
57
|
+
description="Use the chat id created in first_call, leave it as empty string if first_call"
|
|
58
|
+
)
|
|
56
59
|
code_writer_config: Optional[CodeWriterMode] = None
|
|
57
60
|
|
|
58
61
|
def model_post_init(self, __context: Any) -> None:
|
|
@@ -102,6 +105,7 @@ class SendAscii(BaseModel):
|
|
|
102
105
|
class BashCommand(BaseModel):
|
|
103
106
|
action_json: Command | StatusCheck | SendText | SendSpecials | SendAscii
|
|
104
107
|
wait_for_seconds: Optional[float] = None
|
|
108
|
+
chat_id: str
|
|
105
109
|
|
|
106
110
|
|
|
107
111
|
class ReadImage(BaseModel):
|
|
@@ -214,6 +218,7 @@ class FileWriteOrEdit(BaseModel):
|
|
|
214
218
|
file_path: str
|
|
215
219
|
percentage_to_change: int # 0.0 to 100.0
|
|
216
220
|
file_content_or_search_replace_blocks: str
|
|
221
|
+
chat_id: str
|
|
217
222
|
|
|
218
223
|
|
|
219
224
|
class ContextSave(BaseModel):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wcgw
|
|
3
|
-
Version:
|
|
4
|
-
Summary: Shell and coding agent
|
|
3
|
+
Version: 5.0.1
|
|
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>
|
|
7
7
|
License-File: LICENSE
|
|
@@ -28,12 +28,11 @@ Requires-Dist: uvicorn>=0.31.0
|
|
|
28
28
|
Requires-Dist: websockets>=13.1
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
|
-
# Shell and Coding agent for Claude and
|
|
31
|
+
# Shell and Coding agent for Claude and other mcp clients
|
|
32
32
|
|
|
33
33
|
Empowering chat applications to code, build and run on your local machine.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
- Chatgpt - Allows custom gpt to talk to your shell via a relay server. (linux, mac, windows on wsl)
|
|
35
|
+
wcgw is an MCP server with tightly integrated shell and code editing tools.
|
|
37
36
|
|
|
38
37
|
⚠️ Warning: do not allow BashCommand tool without reviewing the command, it may result in data loss.
|
|
39
38
|
|
|
@@ -49,6 +48,8 @@ Empowering chat applications to code, build and run on your local machine.
|
|
|
49
48
|
|
|
50
49
|
## Updates
|
|
51
50
|
|
|
51
|
+
- [27 Apr 2025] Removed support for GPTs over relay server. Only MCP server is supported in version >= 5.
|
|
52
|
+
|
|
52
53
|
- [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
|
|
53
54
|
|
|
54
55
|
- [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
|
|
@@ -119,11 +120,9 @@ Then create or update `claude_desktop_config.json` (~/Library/Application Suppor
|
|
|
119
120
|
"args": [
|
|
120
121
|
"tool",
|
|
121
122
|
"run",
|
|
122
|
-
"--from",
|
|
123
|
-
"wcgw@latest",
|
|
124
123
|
"--python",
|
|
125
124
|
"3.12",
|
|
126
|
-
"
|
|
125
|
+
"wcgw@latest"
|
|
127
126
|
]
|
|
128
127
|
}
|
|
129
128
|
}
|
|
@@ -135,10 +134,10 @@ Then restart claude app.
|
|
|
135
134
|
_If there's an error in setting up_
|
|
136
135
|
|
|
137
136
|
- If there's an error like "uv ENOENT", make sure `uv` is installed. Then run 'which uv' in the terminal, and use its output in place of "uv" in the configuration.
|
|
138
|
-
- If there's still an issue, check that `uv tool run --
|
|
137
|
+
- If there's still an issue, check that `uv tool run --python 3.12 wcgw@latest` runs in your terminal. It should have no output and shouldn't exit.
|
|
139
138
|
- Try removing ~/.cache/uv folder
|
|
140
139
|
- Try using `uv` version `0.6.0` for which this tool was tested.
|
|
141
|
-
- Debug the mcp server using `npx @modelcontextprotocol/inspector@0.1.7 uv tool run --
|
|
140
|
+
- Debug the mcp server using `npx @modelcontextprotocol/inspector@0.1.7 uv tool run --python 3.12 wcgw@latest`
|
|
142
141
|
|
|
143
142
|
### Windows on wsl
|
|
144
143
|
|
|
@@ -157,11 +156,9 @@ Then add or update the claude config file `%APPDATA%\Claude\claude_desktop_confi
|
|
|
157
156
|
"uv",
|
|
158
157
|
"tool",
|
|
159
158
|
"run",
|
|
160
|
-
"--from",
|
|
161
|
-
"wcgw@latest",
|
|
162
159
|
"--python",
|
|
163
160
|
"3.12",
|
|
164
|
-
"
|
|
161
|
+
"wcgw@latest"
|
|
165
162
|
]
|
|
166
163
|
}
|
|
167
164
|
}
|
|
@@ -221,10 +218,6 @@ Commands:
|
|
|
221
218
|
|
|
222
219
|
- Select a text and press `cmd+'` and then enter instructions. This will switch the app to Claude and paste a text containing your instructions, file path, workspace dir, and the selected text.
|
|
223
220
|
|
|
224
|
-
## Chatgpt Setup
|
|
225
|
-
|
|
226
|
-
Read here: https://github.com/rusiaaman/wcgw/blob/main/openai.md
|
|
227
|
-
|
|
228
221
|
## Examples
|
|
229
222
|
|
|
230
223
|

|
|
@@ -261,7 +254,7 @@ Add `OPENAI_API_KEY` and `OPENAI_ORG_ID` env variables.
|
|
|
261
254
|
|
|
262
255
|
Then run
|
|
263
256
|
|
|
264
|
-
`uvx
|
|
257
|
+
`uvx wcgw@latest wcgw_local --limit 0.1` # Cost limit $0.1
|
|
265
258
|
|
|
266
259
|
You can now directly write messages or press enter key to open vim for multiline message and text pasting.
|
|
267
260
|
|
|
@@ -271,7 +264,7 @@ Add `ANTHROPIC_API_KEY` env variable.
|
|
|
271
264
|
|
|
272
265
|
Then run
|
|
273
266
|
|
|
274
|
-
`uvx
|
|
267
|
+
`uvx wcgw@latest wcgw_local --claude`
|
|
275
268
|
|
|
276
269
|
You can now directly write messages or press enter key to open vim for multiline message and text pasting.
|
|
277
270
|
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
wcgw/__init__.py,sha256=
|
|
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=y60Lv_uUA1_sGIfADLUKy7rFPTax8jxor5GGCDKBfZ0,7533
|
|
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=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=roH6SPBokJMr5IzAlccdI-vJyvyS5vqSMMyth7TE86A,10315
|
|
9
|
+
wcgw/client/tool_prompts.py,sha256=bbyKenE38s2CLnUZ9NN5OqqabsJkfidMR_YWocEAzBU,4417
|
|
10
|
+
wcgw/client/tools.py,sha256=P0wFsPJpnq-Bj6kF0PNxib7yqQXCxJ45nzPAH8xO9hs,45809
|
|
11
|
+
wcgw/client/bash_state/bash_state.py,sha256=dY-dbApCenCOaDMC0BlBaSlI5jRrAZqwrVFqaeb0hGA,41670
|
|
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
|
-
wcgw/client/file_ops/diff_edit.py,sha256=
|
|
15
|
+
wcgw/client/file_ops/diff_edit.py,sha256=ePJWUctlQYmopIyuLORLYPE6xliX3UooWXofAI2QJEE,18618
|
|
16
16
|
wcgw/client/file_ops/search_replace.py,sha256=TaIPDqjgmTo4oghhO3zIFklq5JjAbx_aPHJ7yEgvDh4,6854
|
|
17
17
|
wcgw/client/mcp_server/Readme.md,sha256=2Z88jj1mf9daYGW1CWaldcJ0moy8owDumhR2glBY3A8,109
|
|
18
18
|
wcgw/client/mcp_server/__init__.py,sha256=mm7xhBIPwJpRT3u-Qsj4cKVMpVyucJoKRlbMP_gRRB0,343
|
|
@@ -23,9 +23,6 @@ wcgw/client/repo_ops/path_prob.py,sha256=SWf0CDn37rtlsYRQ51ufSxay-heaQoVIhr1alB9
|
|
|
23
23
|
wcgw/client/repo_ops/paths_model.vocab,sha256=M1pXycYDQehMXtpp-qAgU7rtzeBbCOiJo4qcYFY0kqk,315087
|
|
24
24
|
wcgw/client/repo_ops/paths_tokens.model,sha256=jiwwE4ae8ADKuTZISutXuM5Wfyc_FBmN5rxTjoNnCos,1569052
|
|
25
25
|
wcgw/client/repo_ops/repo_context.py,sha256=e_w-1VfxWQiZT3r66N13nlmPt6AGm0uvG3A7aYSgaCI,9632
|
|
26
|
-
wcgw/relay/client.py,sha256=BUeEKUsWts8RpYxXwXcyFyjBJhOCS-CxThAlL_-VCOI,3618
|
|
27
|
-
wcgw/relay/serve.py,sha256=vaHxSm4DkWUKLMOnz2cO6ClR2udnaXCWAGl0O_bXvrs,6984
|
|
28
|
-
wcgw/relay/static/privacy.txt,sha256=s9qBdbx2SexCpC_z33sg16TptmAwDEehMCLz4L50JLc,529
|
|
29
26
|
wcgw_cli/__init__.py,sha256=TNxXsTPgb52OhakIda9wTRh91cqoBqgQRx5TxjzQQFU,21
|
|
30
27
|
wcgw_cli/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
|
31
28
|
wcgw_cli/anthropic_client.py,sha256=R95ht8ct3TgV8PMNzr5yJdAXAmZpozPpyZZ01J9bQHs,19627
|
|
@@ -54,8 +51,8 @@ mcp_wcgw/shared/memory.py,sha256=dBsOghxHz8-tycdSVo9kSujbsC8xb_tYsGmuJobuZnw,281
|
|
|
54
51
|
mcp_wcgw/shared/progress.py,sha256=ymxOsb8XO5Mhlop7fRfdbmvPodANj7oq6O4dD0iUcnw,1048
|
|
55
52
|
mcp_wcgw/shared/session.py,sha256=e44a0LQOW8gwdLs9_DE9oDsxqW2U8mXG3d5KT95bn5o,10393
|
|
56
53
|
mcp_wcgw/shared/version.py,sha256=d2LZii-mgsPIxpshjkXnOTUmk98i0DT4ff8VpA_kAvE,111
|
|
57
|
-
wcgw-
|
|
58
|
-
wcgw-
|
|
59
|
-
wcgw-
|
|
60
|
-
wcgw-
|
|
61
|
-
wcgw-
|
|
54
|
+
wcgw-5.0.1.dist-info/METADATA,sha256=Z4gKqY1UZYQ-kV43rzRuaJ0lqwY4WG5kTgXcn_MFUCQ,14840
|
|
55
|
+
wcgw-5.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
56
|
+
wcgw-5.0.1.dist-info/entry_points.txt,sha256=UnjK-MAH4Qssh0tGJDMeij1oi-oRKokItkknP_BwShE,94
|
|
57
|
+
wcgw-5.0.1.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
|
|
58
|
+
wcgw-5.0.1.dist-info/RECORD,,
|
wcgw/relay/client.py
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import importlib.metadata
|
|
2
|
-
import os
|
|
3
|
-
import time
|
|
4
|
-
import traceback
|
|
5
|
-
import uuid
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
import rich
|
|
9
|
-
import typer
|
|
10
|
-
import websockets
|
|
11
|
-
from typer import Typer
|
|
12
|
-
from websockets.sync.client import connect as syncconnect
|
|
13
|
-
|
|
14
|
-
from ..client.bash_state.bash_state import BashState
|
|
15
|
-
from ..client.tools import Context, curr_cost, default_enc, get_tool_output
|
|
16
|
-
from ..types_ import Mdata
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def register_client(server_url: str, client_uuid: str = "") -> None:
|
|
20
|
-
global default_enc, curr_cost
|
|
21
|
-
# Generate a unique UUID for this client
|
|
22
|
-
if not client_uuid:
|
|
23
|
-
client_uuid = str(uuid.uuid4())
|
|
24
|
-
|
|
25
|
-
# Create the WebSocket connection and context
|
|
26
|
-
the_console = rich.console.Console(style="magenta", highlight=False, markup=False)
|
|
27
|
-
with BashState(
|
|
28
|
-
the_console, os.getcwd(), None, None, None, None, True, None
|
|
29
|
-
) as bash_state:
|
|
30
|
-
context = Context(bash_state=bash_state, console=the_console)
|
|
31
|
-
|
|
32
|
-
try:
|
|
33
|
-
with syncconnect(f"{server_url}/{client_uuid}") as websocket:
|
|
34
|
-
server_version = str(websocket.recv())
|
|
35
|
-
print(f"Server version: {server_version}")
|
|
36
|
-
client_version = importlib.metadata.version("wcgw")
|
|
37
|
-
websocket.send(client_version)
|
|
38
|
-
|
|
39
|
-
print(f"Connected. Share this user id with the chatbot: {client_uuid}")
|
|
40
|
-
while True:
|
|
41
|
-
# Wait to receive data from the server
|
|
42
|
-
message = websocket.recv()
|
|
43
|
-
mdata = Mdata.model_validate_json(message)
|
|
44
|
-
if isinstance(mdata.data, str):
|
|
45
|
-
raise Exception(mdata)
|
|
46
|
-
try:
|
|
47
|
-
outputs, cost = get_tool_output(
|
|
48
|
-
context,
|
|
49
|
-
mdata.data,
|
|
50
|
-
default_enc,
|
|
51
|
-
0.0,
|
|
52
|
-
lambda x, y: ("", 0),
|
|
53
|
-
8000,
|
|
54
|
-
)
|
|
55
|
-
output = outputs[0]
|
|
56
|
-
curr_cost += cost
|
|
57
|
-
print(f"{curr_cost=}")
|
|
58
|
-
except Exception as e:
|
|
59
|
-
output = f"GOT EXCEPTION while calling tool. Error: {e}"
|
|
60
|
-
context.console.print(traceback.format_exc())
|
|
61
|
-
assert isinstance(output, str)
|
|
62
|
-
websocket.send(output)
|
|
63
|
-
|
|
64
|
-
except (websockets.ConnectionClosed, ConnectionError, OSError):
|
|
65
|
-
print(f"Connection closed for UUID: {client_uuid}, retrying")
|
|
66
|
-
time.sleep(0.5)
|
|
67
|
-
register_client(server_url, client_uuid)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
run = Typer(pretty_exceptions_show_locals=False, no_args_is_help=True)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
@run.command()
|
|
74
|
-
def app(
|
|
75
|
-
server_url: str = "",
|
|
76
|
-
client_uuid: Optional[str] = None,
|
|
77
|
-
version: bool = typer.Option(False, "--version", "-v"),
|
|
78
|
-
) -> None:
|
|
79
|
-
if version:
|
|
80
|
-
version_ = importlib.metadata.version("wcgw")
|
|
81
|
-
print(f"wcgw version: {version_}")
|
|
82
|
-
exit()
|
|
83
|
-
if not server_url:
|
|
84
|
-
server_url = os.environ.get("WCGW_RELAY_SERVER", "")
|
|
85
|
-
if not server_url:
|
|
86
|
-
print(
|
|
87
|
-
"Error: Please provide relay server url using --server_url or WCGW_RELAY_SERVER environment variable"
|
|
88
|
-
)
|
|
89
|
-
print(
|
|
90
|
-
"\tNOTE: you need to run a relay server first, author doesn't host a relay server anymore."
|
|
91
|
-
)
|
|
92
|
-
print("\thttps://github.com/rusiaaman/wcgw/blob/main/openai.md")
|
|
93
|
-
print("\tExample `--server-url=ws://localhost:8000/v1/register`")
|
|
94
|
-
raise typer.Exit(1)
|
|
95
|
-
register_client(server_url, client_uuid or "")
|
wcgw/relay/serve.py
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import threading
|
|
3
|
-
import time
|
|
4
|
-
from importlib import metadata
|
|
5
|
-
from typing import Any, Callable, Coroutine, DefaultDict, Optional
|
|
6
|
-
from uuid import UUID
|
|
7
|
-
|
|
8
|
-
import fastapi
|
|
9
|
-
import semantic_version # type: ignore[import-untyped]
|
|
10
|
-
import uvicorn
|
|
11
|
-
from dotenv import load_dotenv
|
|
12
|
-
from fastapi import WebSocket, WebSocketDisconnect
|
|
13
|
-
from fastapi.staticfiles import StaticFiles
|
|
14
|
-
from pydantic import BaseModel
|
|
15
|
-
|
|
16
|
-
from ..types_ import (
|
|
17
|
-
BashCommand,
|
|
18
|
-
ContextSave,
|
|
19
|
-
FileWriteOrEdit,
|
|
20
|
-
Initialize,
|
|
21
|
-
ReadFiles,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class Mdata(BaseModel):
|
|
26
|
-
data: BashCommand | FileWriteOrEdit | ReadFiles | Initialize | ContextSave | str
|
|
27
|
-
user_id: UUID
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
app = fastapi.FastAPI()
|
|
31
|
-
|
|
32
|
-
clients: dict[UUID, Callable[[Mdata], Coroutine[None, None, None]]] = {}
|
|
33
|
-
websockets: dict[UUID, WebSocket] = {}
|
|
34
|
-
gpts: dict[UUID, Callable[[str], None]] = {}
|
|
35
|
-
|
|
36
|
-
images: DefaultDict[UUID, dict[str, dict[str, Any]]] = DefaultDict(dict)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
CLIENT_VERSION_MINIMUM = "2.7.0"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@app.websocket("/v1/register/{uuid}")
|
|
43
|
-
async def register_websocket(websocket: WebSocket, uuid: UUID) -> None:
|
|
44
|
-
await websocket.accept()
|
|
45
|
-
|
|
46
|
-
# send server version
|
|
47
|
-
version = metadata.version("wcgw")
|
|
48
|
-
await websocket.send_text(version)
|
|
49
|
-
|
|
50
|
-
# receive client version
|
|
51
|
-
client_version = await websocket.receive_text()
|
|
52
|
-
sem_version_client = semantic_version.Version.coerce(client_version)
|
|
53
|
-
sem_version_server = semantic_version.Version.coerce(CLIENT_VERSION_MINIMUM)
|
|
54
|
-
if sem_version_client < sem_version_server:
|
|
55
|
-
await websocket.send_text(
|
|
56
|
-
Mdata(
|
|
57
|
-
user_id=uuid,
|
|
58
|
-
data=f"Client version {client_version} is outdated. Please upgrade to {CLIENT_VERSION_MINIMUM} or higher.",
|
|
59
|
-
).model_dump_json()
|
|
60
|
-
)
|
|
61
|
-
await websocket.close(
|
|
62
|
-
reason="Client version outdated. Please upgrade to the latest version.",
|
|
63
|
-
code=1002,
|
|
64
|
-
)
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
# Register the callback for this client UUID
|
|
68
|
-
async def send_data_callback(data: Mdata) -> None:
|
|
69
|
-
await websocket.send_text(data.model_dump_json())
|
|
70
|
-
|
|
71
|
-
clients[uuid] = send_data_callback
|
|
72
|
-
websockets[uuid] = websocket
|
|
73
|
-
|
|
74
|
-
try:
|
|
75
|
-
while True:
|
|
76
|
-
received_data = await websocket.receive_text()
|
|
77
|
-
if uuid not in gpts:
|
|
78
|
-
raise fastapi.HTTPException(status_code=400, detail="No call made")
|
|
79
|
-
gpts[uuid](received_data)
|
|
80
|
-
except WebSocketDisconnect:
|
|
81
|
-
# Remove the client if the WebSocket is disconnected
|
|
82
|
-
del clients[uuid]
|
|
83
|
-
del websockets[uuid]
|
|
84
|
-
print(f"Client {uuid} disconnected")
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
class FileWriteOrEdithUUID(FileWriteOrEdit):
|
|
88
|
-
user_id: UUID
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
@app.post("/v1/file_write_or_edit")
|
|
92
|
-
async def file_write_or_edit(write_file_data: FileWriteOrEdithUUID) -> str:
|
|
93
|
-
user_id = write_file_data.user_id
|
|
94
|
-
if user_id not in clients:
|
|
95
|
-
return "Failure: id not found, ask the user to check it."
|
|
96
|
-
|
|
97
|
-
results: Optional[str] = None
|
|
98
|
-
|
|
99
|
-
def put_results(result: str) -> None:
|
|
100
|
-
nonlocal results
|
|
101
|
-
results = result
|
|
102
|
-
|
|
103
|
-
gpts[user_id] = put_results
|
|
104
|
-
|
|
105
|
-
await clients[user_id](Mdata(data=write_file_data, user_id=user_id))
|
|
106
|
-
|
|
107
|
-
start_time = time.time()
|
|
108
|
-
while time.time() - start_time < 30:
|
|
109
|
-
if results is not None:
|
|
110
|
-
return results
|
|
111
|
-
await asyncio.sleep(0.1)
|
|
112
|
-
|
|
113
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class CommandWithUUID(BashCommand):
|
|
117
|
-
user_id: UUID
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@app.post("/v1/bash_command")
|
|
121
|
-
async def bash_command(command: CommandWithUUID) -> str:
|
|
122
|
-
user_id = command.user_id
|
|
123
|
-
if user_id not in clients:
|
|
124
|
-
return "Failure: id not found, ask the user to check it."
|
|
125
|
-
|
|
126
|
-
results: Optional[str] = None
|
|
127
|
-
|
|
128
|
-
def put_results(result: str) -> None:
|
|
129
|
-
nonlocal results
|
|
130
|
-
results = result
|
|
131
|
-
|
|
132
|
-
gpts[user_id] = put_results
|
|
133
|
-
|
|
134
|
-
await clients[user_id](
|
|
135
|
-
Mdata(
|
|
136
|
-
data=BashCommand(
|
|
137
|
-
action_json=command.action_json,
|
|
138
|
-
wait_for_seconds=command.wait_for_seconds,
|
|
139
|
-
),
|
|
140
|
-
user_id=user_id,
|
|
141
|
-
)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
start_time = time.time()
|
|
145
|
-
while time.time() - start_time < 30:
|
|
146
|
-
if results is not None:
|
|
147
|
-
return results
|
|
148
|
-
await asyncio.sleep(0.1)
|
|
149
|
-
|
|
150
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
class ReadFileWithUUID(ReadFiles):
|
|
154
|
-
user_id: UUID
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@app.post("/v1/read_file")
|
|
158
|
-
async def read_file_endpoint(read_file_data: ReadFileWithUUID) -> str:
|
|
159
|
-
user_id = read_file_data.user_id
|
|
160
|
-
if user_id not in clients:
|
|
161
|
-
return "Failure: id not found, ask the user to check it."
|
|
162
|
-
|
|
163
|
-
results: Optional[str] = None
|
|
164
|
-
|
|
165
|
-
def put_results(result: str) -> None:
|
|
166
|
-
nonlocal results
|
|
167
|
-
results = result
|
|
168
|
-
|
|
169
|
-
gpts[user_id] = put_results
|
|
170
|
-
|
|
171
|
-
await clients[user_id](Mdata(data=read_file_data, user_id=user_id))
|
|
172
|
-
|
|
173
|
-
start_time = time.time()
|
|
174
|
-
while time.time() - start_time < 30:
|
|
175
|
-
if results is not None:
|
|
176
|
-
return results
|
|
177
|
-
await asyncio.sleep(0.1)
|
|
178
|
-
|
|
179
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class InitializeWithUUID(Initialize):
|
|
183
|
-
user_id: UUID
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@app.post("/v1/initialize")
|
|
187
|
-
async def initialize(initialize_data: InitializeWithUUID) -> str:
|
|
188
|
-
user_id = initialize_data.user_id
|
|
189
|
-
if user_id not in clients:
|
|
190
|
-
return "Failure: id not found, ask the user to check it."
|
|
191
|
-
|
|
192
|
-
results: Optional[str] = None
|
|
193
|
-
|
|
194
|
-
def put_results(result: str) -> None:
|
|
195
|
-
nonlocal results
|
|
196
|
-
results = result
|
|
197
|
-
|
|
198
|
-
gpts[user_id] = put_results
|
|
199
|
-
|
|
200
|
-
await clients[user_id](Mdata(data=initialize_data, user_id=user_id))
|
|
201
|
-
|
|
202
|
-
start_time = time.time()
|
|
203
|
-
while time.time() - start_time < 30:
|
|
204
|
-
if results is not None:
|
|
205
|
-
return results
|
|
206
|
-
await asyncio.sleep(0.1)
|
|
207
|
-
|
|
208
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
class ContextSaveWithUUID(ContextSave):
|
|
212
|
-
user_id: UUID
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
@app.post("/v1/context_save")
|
|
216
|
-
async def context_save(context_save_data: ContextSaveWithUUID) -> str:
|
|
217
|
-
user_id = context_save_data.user_id
|
|
218
|
-
if user_id not in clients:
|
|
219
|
-
return "Failure: id not found, ask the user to check it."
|
|
220
|
-
|
|
221
|
-
results: Optional[str] = None
|
|
222
|
-
|
|
223
|
-
def put_results(result: str) -> None:
|
|
224
|
-
nonlocal results
|
|
225
|
-
results = result
|
|
226
|
-
|
|
227
|
-
gpts[user_id] = put_results
|
|
228
|
-
|
|
229
|
-
await clients[user_id](Mdata(data=context_save_data, user_id=user_id))
|
|
230
|
-
|
|
231
|
-
start_time = time.time()
|
|
232
|
-
while time.time() - start_time < 30:
|
|
233
|
-
if results is not None:
|
|
234
|
-
return results
|
|
235
|
-
await asyncio.sleep(0.1)
|
|
236
|
-
|
|
237
|
-
raise fastapi.HTTPException(status_code=500, detail="Timeout error")
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
app.mount("/static", StaticFiles(directory="static"))
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def run() -> None:
|
|
244
|
-
load_dotenv()
|
|
245
|
-
|
|
246
|
-
uvicorn_thread = threading.Thread(
|
|
247
|
-
target=uvicorn.run,
|
|
248
|
-
args=(app,),
|
|
249
|
-
kwargs={
|
|
250
|
-
"host": "0.0.0.0",
|
|
251
|
-
"port": 8000,
|
|
252
|
-
"log_level": "info",
|
|
253
|
-
"access_log": True,
|
|
254
|
-
},
|
|
255
|
-
)
|
|
256
|
-
uvicorn_thread.start()
|
|
257
|
-
uvicorn_thread.join()
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if __name__ == "__main__":
|
|
261
|
-
run()
|
wcgw/relay/static/privacy.txt
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
Privacy Policy
|
|
2
|
-
I do not collect, store, or share any personal data.
|
|
3
|
-
The data from your terminal is not stored anywhere and it's not logged or collected in any form.
|
|
4
|
-
There is a relay webserver for connecting your terminal to chatgpt the source code for which is open at https://github.com/rusiaaman/wcgw/tree/main/src/relay that you can run on your own.
|
|
5
|
-
Other than the relay webserver there is no further involvement of my servers or services.
|
|
6
|
-
Feel free to me contact at info@arcfu.com for questions on privacy or anything else.
|
|
7
|
-
|
|
File without changes
|
|
File without changes
|