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 CHANGED
@@ -1,2 +1,4 @@
1
1
  from .client.mcp_server import main as mcp_server
2
- from .relay.client import run as listen
2
+
3
+ # Export mcp_server as the default entry point for wcgw
4
+ listen = mcp_server
@@ -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 = """
@@ -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 wcgw.client.bash_state.bash_state import get_status
32
- from wcgw.client.repo_ops.file_stats import (
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
- if type == "first_call":
116
- if task_id_to_resume:
117
- try:
118
- project_root_path, task_mem, loaded_state = load_memory(
119
- task_id_to_resume,
120
- max_tokens,
121
- lambda x: default_enc.encoder(x),
122
- lambda x: default_enc.decoder(x),
123
- )
124
- memory = "Following is the retrieved task:\n" + task_mem
125
- if os.path.exists(project_root_path):
126
- any_workspace_path = project_root_path
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
- except Exception:
129
- memory = f'Error: Unable to load task with ID "{task_id_to_resume}" '
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
- global INITIALIZED
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, INITIALIZED
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(action_json=Command(command="pwd")),
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.1.2
4
- Summary: Shell and coding agent on claude and chatgpt
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 Chatgpt
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
- - Claude - MCP server with tightly integrated shell and code editing tools.
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
- "wcgw_mcp"
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 --from wcgw@latest --python 3.12 wcgw_mcp` runs in your terminal. It should have no output and shouldn't exit.
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 --from wcgw@latest --python 3.12 wcgw_mcp`
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
- "wcgw_mcp"
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
  ![example](https://github.com/rusiaaman/wcgw/blob/main/static/example.jpg?raw=true)
@@ -261,7 +254,7 @@ Add `OPENAI_API_KEY` and `OPENAI_ORG_ID` env variables.
261
254
 
262
255
  Then run
263
256
 
264
- `uvx --from wcgw@latest wcgw_local --limit 0.1` # Cost limit $0.1
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 --from wcgw@latest wcgw_local --claude`
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=qUofQOAXCGcWr2u_B8U-MIMhhYaBUpUwNDcscvRmYfo,90
1
+ wcgw/__init__.py,sha256=JgAY25VsA208v8E7QTIU0E50nsk-TCJ4FWTEHmnssYU,127
2
2
  wcgw/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- wcgw/types_.py,sha256=mwr2x20yLF85OoHy3pKxtmG834Gnoqrkx3pA8skGsVk,7360
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=gXm0u5EGQuPYEPZnyAptdzusN0JzLMED_DyErojPE6s,10679
9
- wcgw/client/tool_prompts.py,sha256=zHJQHb_E44zA3rBIkAudibgP3Zbw0DjA-CKM8qlqmMU,4410
10
- wcgw/client/tools.py,sha256=JXk6t7iNtP2EK6mdTkMqCquGGQ6TtoIJ8AEhkSith14,43963
11
- wcgw/client/bash_state/bash_state.py,sha256=hbLBRcBhFCGqHKwxl9b3R1XXAvBLm0P7N7iYpDOh1CM,38190
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=eNxFRmVsBfY0ISwfsV5s8rlMwMxTlm6ko150iFFCsT8,18525
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-4.1.2.dist-info/METADATA,sha256=MDFrRbNsGOwNKkOT6hxkKrVc8Knmj3-E93xpjb8zQiQ,15020
58
- wcgw-4.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
59
- wcgw-4.1.2.dist-info/entry_points.txt,sha256=vd3tj1_Kzfp55LscJ8-6WFMM5hm9cWTfNGFCrWBnH3Q,124
60
- wcgw-4.1.2.dist-info/licenses/LICENSE,sha256=BvY8xqjOfc3X2qZpGpX3MZEmF-4Dp0LqgKBbT6L_8oI,11142
61
- wcgw-4.1.2.dist-info/RECORD,,
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,,
@@ -1,5 +1,4 @@
1
1
  [console_scripts]
2
- wcgw = wcgw:listen
2
+ wcgw = wcgw:mcp_server
3
3
  wcgw_local = wcgw_cli:app
4
4
  wcgw_mcp = wcgw:mcp_server
5
- wcgw_relay = wcgw.relay.serve:run
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()
@@ -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