wcgw 5.0.2__py3-none-any.whl → 5.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wcgw might be problematic. Click here for more details.

Files changed (39) hide show
  1. wcgw/client/bash_state/bash_state.py +2 -2
  2. wcgw/client/file_ops/diff_edit.py +14 -2
  3. wcgw/client/file_ops/extensions.py +137 -0
  4. wcgw/client/file_ops/search_replace.py +1 -2
  5. wcgw/client/mcp_server/server.py +10 -18
  6. wcgw/client/memory.py +4 -1
  7. wcgw/client/repo_ops/display_tree.py +4 -4
  8. wcgw/client/tool_prompts.py +16 -15
  9. wcgw/client/tools.py +95 -38
  10. {wcgw-5.0.2.dist-info → wcgw-5.1.1.dist-info}/METADATA +6 -18
  11. wcgw-5.1.1.dist-info/RECORD +37 -0
  12. wcgw_cli/anthropic_client.py +8 -4
  13. wcgw_cli/openai_client.py +7 -3
  14. mcp_wcgw/__init__.py +0 -114
  15. mcp_wcgw/client/__init__.py +0 -0
  16. mcp_wcgw/client/__main__.py +0 -79
  17. mcp_wcgw/client/session.py +0 -234
  18. mcp_wcgw/client/sse.py +0 -142
  19. mcp_wcgw/client/stdio.py +0 -128
  20. mcp_wcgw/py.typed +0 -0
  21. mcp_wcgw/server/__init__.py +0 -514
  22. mcp_wcgw/server/__main__.py +0 -50
  23. mcp_wcgw/server/models.py +0 -16
  24. mcp_wcgw/server/session.py +0 -288
  25. mcp_wcgw/server/sse.py +0 -178
  26. mcp_wcgw/server/stdio.py +0 -83
  27. mcp_wcgw/server/websocket.py +0 -61
  28. mcp_wcgw/shared/__init__.py +0 -0
  29. mcp_wcgw/shared/context.py +0 -14
  30. mcp_wcgw/shared/exceptions.py +0 -9
  31. mcp_wcgw/shared/memory.py +0 -87
  32. mcp_wcgw/shared/progress.py +0 -40
  33. mcp_wcgw/shared/session.py +0 -288
  34. mcp_wcgw/shared/version.py +0 -3
  35. mcp_wcgw/types.py +0 -1060
  36. wcgw-5.0.2.dist-info/RECORD +0 -58
  37. {wcgw-5.0.2.dist-info → wcgw-5.1.1.dist-info}/WHEEL +0 -0
  38. {wcgw-5.0.2.dist-info → wcgw-5.1.1.dist-info}/entry_points.txt +0 -0
  39. {wcgw-5.0.2.dist-info → wcgw-5.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -996,7 +996,7 @@ def execute_bash(
996
996
  bash_state: BashState,
997
997
  enc: EncoderDecoder[int],
998
998
  bash_arg: BashCommand,
999
- max_tokens: Optional[int],
999
+ max_tokens: Optional[int], # This will be noncoding_max_tokens
1000
1000
  timeout_s: Optional[float],
1001
1001
  ) -> tuple[str, float]:
1002
1002
  try:
@@ -1026,7 +1026,7 @@ def _execute_bash(
1026
1026
  bash_state: BashState,
1027
1027
  enc: EncoderDecoder[int],
1028
1028
  bash_arg: BashCommand,
1029
- max_tokens: Optional[int],
1029
+ max_tokens: Optional[int], # This will be noncoding_max_tokens
1030
1030
  timeout_s: Optional[float],
1031
1031
  ) -> tuple[str, float]:
1032
1032
  try:
@@ -46,10 +46,13 @@ class FileEditOutput:
46
46
  last_idx = 0
47
47
  errors = []
48
48
  warnings = set[str]()
49
+ info = set[str]()
50
+ score = 0.0
49
51
  for (span, tolerances, replace_with), search_ in zip(
50
52
  self.edited_with_tolerances, self.orig_search_blocks
51
53
  ):
52
54
  for tol in tolerances:
55
+ score += tol.count * tol.score_multiplier
53
56
  if tol.count > 0:
54
57
  if tol.severity_cat == "WARNING":
55
58
  warnings.add(tol.error_name)
@@ -66,6 +69,8 @@ Error:
66
69
  {tol.error_name}
67
70
  ---
68
71
  """)
72
+ else:
73
+ info.add(tol.error_name)
69
74
  if len(errors) >= max_errors:
70
75
  raise SearchReplaceMatchError("\n".join(errors))
71
76
  if last_idx < span.start:
@@ -80,12 +85,19 @@ Error:
80
85
  if errors:
81
86
  raise SearchReplaceMatchError("\n".join(errors))
82
87
 
88
+ if score > 1000:
89
+ display = (list(warnings) + list(info))[:max_errors]
90
+ raise SearchReplaceMatchError(
91
+ "Too many warnings generated, not apply the edits\n"
92
+ + "\n".join(display)
93
+ )
94
+
83
95
  return new_lines, set(warnings)
84
96
 
85
97
  @staticmethod
86
98
  def get_best_match(
87
99
  outputs: list["FileEditOutput"],
88
- ) -> tuple[list["FileEditOutput"], bool]:
100
+ ) -> list["FileEditOutput"]:
89
101
  best_hits: list[FileEditOutput] = []
90
102
  best_score = float("-inf")
91
103
  assert outputs
@@ -103,7 +115,7 @@ Error:
103
115
  best_score = hit_score
104
116
  elif abs(hit_score - best_score) < 1e-3:
105
117
  best_hits.append(output)
106
- return best_hits, best_score > 1000
118
+ return best_hits
107
119
 
108
120
 
109
121
  def line_process_max_space_tolerance(line: str) -> str:
@@ -0,0 +1,137 @@
1
+ """
2
+ File with definitions of known source code file extensions.
3
+ Used to determine the appropriate context length for files.
4
+ Supports selecting between coding_max_tokens and noncoding_max_tokens
5
+ based on file extensions.
6
+ """
7
+ from typing import Dict, Optional, Set
8
+
9
+ # Set of file extensions considered to be source code
10
+ # Each extension should be listed without the dot (e.g., 'py' not '.py')
11
+ SOURCE_CODE_EXTENSIONS: Set[str] = {
12
+ # Python
13
+ 'py', 'pyx', 'pyi', 'pyw',
14
+
15
+ # JavaScript and TypeScript
16
+ 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs',
17
+
18
+ # Web
19
+ 'html', 'htm', 'xhtml', 'css', 'scss', 'sass', 'less',
20
+
21
+ # C and C++
22
+ 'c', 'h', 'cpp', 'cxx', 'cc', 'hpp', 'hxx', 'hh', 'inl',
23
+
24
+ # C#
25
+ 'cs', 'csx',
26
+
27
+ # Java
28
+ 'java', 'scala', 'kt', 'kts', 'groovy',
29
+
30
+ # Go
31
+ 'go', 'mod',
32
+
33
+ # Rust
34
+ 'rs', 'rlib',
35
+
36
+ # Swift
37
+ 'swift',
38
+
39
+ # Ruby
40
+ 'rb', 'rake', 'gemspec',
41
+
42
+ # PHP
43
+ 'php', 'phtml', 'phar', 'phps',
44
+
45
+ # Shell
46
+ 'sh', 'bash', 'zsh', 'fish',
47
+
48
+ # PowerShell
49
+ 'ps1', 'psm1', 'psd1',
50
+
51
+ # SQL
52
+ 'sql', 'ddl', 'dml',
53
+
54
+ # Markup and config
55
+ 'xml', 'json', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf',
56
+
57
+ # Documentation
58
+ 'md', 'markdown', 'rst', 'adoc', 'tex',
59
+
60
+ # Build and dependency files
61
+ 'Makefile', 'Dockerfile', 'Jenkinsfile',
62
+
63
+ # Haskell
64
+ 'hs', 'lhs',
65
+
66
+ # Lisp family
67
+ 'lisp', 'cl', 'el', 'clj', 'cljs', 'edn', 'scm',
68
+
69
+ # Erlang and Elixir
70
+ 'erl', 'hrl', 'ex', 'exs',
71
+
72
+ # Dart and Flutter
73
+ 'dart',
74
+
75
+ # Objective-C
76
+ 'm', 'mm',
77
+ }
78
+
79
+ # Context length limits based on file type (in tokens)
80
+ CONTEXT_LENGTH_LIMITS: Dict[str, int] = {
81
+ 'source_code': 24000, # For known source code files
82
+ 'default': 8000, # For all other files
83
+ }
84
+
85
+ def is_source_code_file(filename: str) -> bool:
86
+ """
87
+ Determine if a file is a source code file based on its extension.
88
+
89
+ Args:
90
+ filename: The name of the file to check
91
+
92
+ Returns:
93
+ True if the file has a recognized source code extension, False otherwise
94
+ """
95
+ # Extract extension (without the dot)
96
+ parts = filename.split('.')
97
+ if len(parts) > 1:
98
+ ext = parts[-1].lower()
99
+ return ext in SOURCE_CODE_EXTENSIONS
100
+
101
+ # Files without extensions (like 'Makefile', 'Dockerfile')
102
+ # Case-insensitive match for files without extensions
103
+ return filename.lower() in {ext.lower() for ext in SOURCE_CODE_EXTENSIONS}
104
+
105
+ def get_context_length_for_file(filename: str) -> int:
106
+ """
107
+ Get the appropriate context length limit for a file based on its extension.
108
+
109
+ Args:
110
+ filename: The name of the file to check
111
+
112
+ Returns:
113
+ The context length limit in tokens
114
+ """
115
+ if is_source_code_file(filename):
116
+ return CONTEXT_LENGTH_LIMITS['source_code']
117
+ return CONTEXT_LENGTH_LIMITS['default']
118
+
119
+
120
+ def select_max_tokens(filename: str, coding_max_tokens: Optional[int], noncoding_max_tokens: Optional[int]) -> Optional[int]:
121
+ """
122
+ Select the appropriate max_tokens limit based on file type.
123
+
124
+ Args:
125
+ filename: The name of the file to check
126
+ coding_max_tokens: Maximum tokens for source code files
127
+ noncoding_max_tokens: Maximum tokens for non-source code files
128
+
129
+ Returns:
130
+ The appropriate max_tokens limit for the file
131
+ """
132
+ if coding_max_tokens is None and noncoding_max_tokens is None:
133
+ return None
134
+
135
+ if is_source_code_file(filename):
136
+ return coding_max_tokens
137
+ return noncoding_max_tokens
@@ -155,7 +155,7 @@ def edit_with_individual_fallback(
155
155
  original_lines: list[str], search_replace_blocks: list[tuple[list[str], list[str]]]
156
156
  ) -> tuple[list[str], set[str]]:
157
157
  outputs = FileEditInput(original_lines, 0, search_replace_blocks, 0).edit_file()
158
- best_matches, is_error = FileEditOutput.get_best_match(outputs)
158
+ best_matches = FileEditOutput.get_best_match(outputs)
159
159
 
160
160
  try:
161
161
  edited_content, comments_ = best_matches[0].replace_or_throw(3)
@@ -171,7 +171,6 @@ def edit_with_individual_fallback(
171
171
  all_comments |= comments_
172
172
  return running_lines, all_comments
173
173
  raise
174
- assert not is_error
175
174
 
176
175
  if len(best_matches) > 1:
177
176
  # Find the first block that differs across matches
@@ -1,13 +1,12 @@
1
1
  import importlib
2
2
  import logging
3
3
  import os
4
- from typing import Any
4
+ from typing import Any, Optional
5
5
 
6
- import mcp_wcgw.server.stdio
7
- import mcp_wcgw.types as types
8
- from mcp_wcgw.server import NotificationOptions, Server
9
- from mcp_wcgw.server.models import InitializationOptions
10
- from mcp_wcgw.types import Tool as ToolParam
6
+ import mcp.server.stdio
7
+ import mcp.types as types
8
+ from mcp.server import NotificationOptions, Server
9
+ from mcp.server.models import InitializationOptions
11
10
  from pydantic import AnyUrl
12
11
 
13
12
  from wcgw.client.modes import KTS
@@ -25,7 +24,7 @@ from ..tools import (
25
24
  which_tool_name,
26
25
  )
27
26
 
28
- server = Server("wcgw")
27
+ server: Server[Any] = Server("wcgw")
29
28
 
30
29
  # Log only time stamp
31
30
  logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(message)s")
@@ -89,15 +88,7 @@ async def handle_list_tools() -> list[types.Tool]:
89
88
  Each tool specifies its arguments using JSON Schema validation.
90
89
  """
91
90
 
92
- tools_ = [
93
- ToolParam(
94
- inputSchema=tool.inputSchema,
95
- name=tool.name,
96
- description=tool.description,
97
- )
98
- for tool in TOOL_PROMPTS
99
- ]
100
- return tools_
91
+ return TOOL_PROMPTS
101
92
 
102
93
 
103
94
  @server.call_tool() # type: ignore
@@ -119,7 +110,8 @@ async def handle_call_tool(
119
110
  default_enc,
120
111
  0.0,
121
112
  lambda x, y: ("", 0),
122
- 8000,
113
+ 24000, # coding_max_tokens
114
+ 8000, # noncoding_max_tokens
123
115
  )
124
116
 
125
117
  except Exception as e:
@@ -165,7 +157,7 @@ async def main() -> None:
165
157
  ) as BASH_STATE:
166
158
  BASH_STATE.console.log("wcgw version: " + version)
167
159
  # Run the server using stdin/stdout streams
168
- async with mcp_wcgw.server.stdio.stdio_server() as (read_stream, write_stream):
160
+ async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
169
161
  await server.run(
170
162
  read_stream,
171
163
  write_stream,
wcgw/client/memory.py CHANGED
@@ -64,7 +64,8 @@ T = TypeVar("T")
64
64
 
65
65
  def load_memory(
66
66
  task_id: str,
67
- max_tokens: Optional[int],
67
+ coding_max_tokens: Optional[int],
68
+ noncoding_max_tokens: Optional[int],
68
69
  encoder: Callable[[str], list[T]],
69
70
  decoder: Callable[[list[T]], str],
70
71
  ) -> tuple[str, str, Optional[dict[str, Any]]]:
@@ -75,6 +76,8 @@ def load_memory(
75
76
  with open(memory_file, "r") as f:
76
77
  data = f.read()
77
78
 
79
+ # Memory files are considered non-code files for token limits
80
+ max_tokens = noncoding_max_tokens
78
81
  if max_tokens:
79
82
  toks = encoder(data)
80
83
  if len(toks) > max_tokens:
@@ -15,7 +15,7 @@ class DirectoryTree:
15
15
  self.root = root
16
16
  self.max_files = max_files
17
17
  self.expanded_files: Set[Path] = set()
18
- self.expanded_dirs = set[Path]()
18
+ self.expanded_dirs: Set[Path] = set()
19
19
 
20
20
  if not self.root.exists():
21
21
  raise ValueError(f"Root path {root} does not exist")
@@ -77,11 +77,11 @@ class DirectoryTree:
77
77
  def _display_recursive(
78
78
  current_path: Path, indent: int = 0, depth: int = 0
79
79
  ) -> None:
80
- # Print current directory name
80
+ # Print current directory name with a trailing slash for directories
81
81
  if current_path == self.root:
82
- writer.write(f"{current_path}\n")
82
+ writer.write(f"{current_path}/\n")
83
83
  else:
84
- writer.write(f"{' ' * indent}{current_path.name}\n")
84
+ writer.write(f"{' ' * indent}{current_path.name}/\n")
85
85
 
86
86
  # Don't recurse beyond depth 1 unless path contains expanded files
87
87
  if depth > 0 and current_path not in self.expanded_dirs:
@@ -1,6 +1,6 @@
1
1
  import os
2
- from dataclasses import dataclass
3
- from typing import Any
2
+
3
+ from mcp.types import Tool, ToolAnnotations
4
4
 
5
5
  from ..types_ import (
6
6
  BashCommand,
@@ -15,15 +15,8 @@ with open(os.path.join(os.path.dirname(__file__), "diff-instructions.txt")) as f
15
15
  diffinstructions = f.read()
16
16
 
17
17
 
18
- @dataclass
19
- class Prompts:
20
- inputSchema: dict[str, Any]
21
- name: str
22
- description: str
23
-
24
-
25
18
  TOOL_PROMPTS = [
26
- Prompts(
19
+ Tool(
27
20
  inputSchema=Initialize.model_json_schema(),
28
21
  name="Initialize",
29
22
  description="""
@@ -38,8 +31,9 @@ TOOL_PROMPTS = [
38
31
  - Use type="reset_shell" if in a conversation shell is not working after multiple tries.
39
32
  - Use type="user_asked_change_workspace" if in a conversation user asked to change workspace
40
33
  """,
34
+ annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
41
35
  ),
42
- Prompts(
36
+ Tool(
43
37
  inputSchema=BashCommand.model_json_schema(),
44
38
  name="BashCommand",
45
39
  description="""
@@ -54,8 +48,9 @@ TOOL_PROMPTS = [
54
48
  - Programs don't hang easily, so most likely explanation for no output is usually that the program is still running, and you need to check status again.
55
49
  - Do not send Ctrl-c before checking for status till 10 minutes or whatever is appropriate for the program to finish.
56
50
  """,
51
+ annotations=ToolAnnotations(destructiveHint=True, openWorldHint=True),
57
52
  ),
58
- Prompts(
53
+ Tool(
59
54
  inputSchema=ReadFiles.model_json_schema(),
60
55
  name="ReadFiles",
61
56
  description="""
@@ -65,13 +60,15 @@ TOOL_PROMPTS = [
65
60
  - You may populate "show_line_numbers_reason" with your reason, by default null/empty means no line numbers are shown.
66
61
  - You may extract a range of lines. E.g., `/path/to/file:1-10` for lines 1-10. You can drop start or end like `/path/to/file:1-` or `/path/to/file:-10`
67
62
  """,
63
+ annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
68
64
  ),
69
- Prompts(
65
+ Tool(
70
66
  inputSchema=ReadImage.model_json_schema(),
71
67
  name="ReadImage",
72
68
  description="Read an image from the shell.",
69
+ annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
73
70
  ),
74
- Prompts(
71
+ Tool(
75
72
  inputSchema=FileWriteOrEdit.model_json_schema(),
76
73
  name="FileWriteOrEdit",
77
74
  description="""
@@ -85,13 +82,17 @@ TOOL_PROMPTS = [
85
82
 
86
83
  """
87
84
  + diffinstructions,
85
+ annotations=ToolAnnotations(
86
+ destructiveHint=True, idempotentHint=True, openWorldHint=False
87
+ ),
88
88
  ),
89
- Prompts(
89
+ Tool(
90
90
  inputSchema=ContextSave.model_json_schema(),
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
94
  - Provide random 3 word unqiue id or whatever user provided.
95
95
  - Leave project path as empty string if no project path""",
96
+ annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False),
96
97
  ),
97
98
  ]