wcgw 4.0.0__tar.gz → 4.1.0__tar.gz

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 (128) hide show
  1. wcgw-4.1.0/CLAUDE.md +41 -0
  2. {wcgw-4.0.0 → wcgw-4.1.0}/PKG-INFO +5 -3
  3. {wcgw-4.0.0 → wcgw-4.1.0}/README.md +4 -2
  4. {wcgw-4.0.0 → wcgw-4.1.0}/pyproject.toml +1 -1
  5. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/bash_state/bash_state.py +9 -7
  6. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/file_ops/diff_edit.py +42 -46
  7. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/file_ops/search_replace.py +74 -55
  8. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/mcp_server/server.py +7 -3
  9. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/modes.py +12 -3
  10. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/repo_ops/repo_context.py +34 -11
  11. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/tool_prompts.py +1 -0
  12. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/tools.py +17 -1
  13. {wcgw-4.0.0 → wcgw-4.1.0}/tests/test_edit.py +122 -0
  14. {wcgw-4.0.0 → wcgw-4.1.0}/uv.lock +1 -1
  15. {wcgw-4.0.0 → wcgw-4.1.0}/.github/workflows/python-publish.yml +0 -0
  16. {wcgw-4.0.0 → wcgw-4.1.0}/.github/workflows/python-tests.yml +0 -0
  17. {wcgw-4.0.0 → wcgw-4.1.0}/.github/workflows/python-types.yml +0 -0
  18. {wcgw-4.0.0 → wcgw-4.1.0}/.gitignore +0 -0
  19. {wcgw-4.0.0 → wcgw-4.1.0}/.gitmodules +0 -0
  20. {wcgw-4.0.0 → wcgw-4.1.0}/.python-version +0 -0
  21. {wcgw-4.0.0 → wcgw-4.1.0}/.vscode/settings.json +0 -0
  22. {wcgw-4.0.0 → wcgw-4.1.0}/Dockerfile +0 -0
  23. {wcgw-4.0.0 → wcgw-4.1.0}/LICENSE +0 -0
  24. {wcgw-4.0.0 → wcgw-4.1.0}/gpt_action_json_schema.json +0 -0
  25. {wcgw-4.0.0 → wcgw-4.1.0}/gpt_instructions.txt +0 -0
  26. {wcgw-4.0.0 → wcgw-4.1.0}/openai.md +0 -0
  27. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.git +0 -0
  28. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  29. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  30. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
  31. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
  32. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
  33. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
  34. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.gitignore +0 -0
  35. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/.python-version +0 -0
  36. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
  37. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
  38. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/LICENSE +0 -0
  39. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/README.md +0 -0
  40. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/RELEASE.md +0 -0
  41. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/SECURITY.md +0 -0
  42. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/README.md +0 -0
  43. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
  44. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
  45. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
  46. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
  47. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
  48. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
  49. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
  50. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
  51. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
  52. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
  53. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
  54. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
  55. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
  56. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
  57. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
  58. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
  59. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
  60. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
  61. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/pyproject.toml +0 -0
  62. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
  63. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
  64. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
  65. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
  66. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
  67. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
  68. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
  69. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
  70. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
  71. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
  72. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
  73. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
  74. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
  75. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
  76. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
  77. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
  78. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
  79. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
  80. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
  81. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
  82. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
  83. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
  84. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
  85. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
  86. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
  87. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
  88. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
  89. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
  90. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
  91. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
  92. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
  93. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
  94. {wcgw-4.0.0 → wcgw-4.1.0}/src/mcp_wcgw_fork/uv.lock +0 -0
  95. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/__init__.py +0 -0
  96. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/__init__.py +0 -0
  97. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/common.py +0 -0
  98. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/diff-instructions.txt +0 -0
  99. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/encoder/__init__.py +0 -0
  100. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/mcp_server/Readme.md +0 -0
  101. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/mcp_server/__init__.py +0 -0
  102. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/memory.py +0 -0
  103. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/repo_ops/display_tree.py +0 -0
  104. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/repo_ops/file_stats.py +0 -0
  105. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/repo_ops/path_prob.py +0 -0
  106. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
  107. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
  108. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/py.typed +0 -0
  109. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/relay/client.py +0 -0
  110. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/relay/serve.py +0 -0
  111. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/relay/static/privacy.txt +0 -0
  112. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw/types_.py +0 -0
  113. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw_cli/__init__.py +0 -0
  114. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw_cli/__main__.py +0 -0
  115. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw_cli/anthropic_client.py +0 -0
  116. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw_cli/cli.py +0 -0
  117. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw_cli/openai_client.py +0 -0
  118. {wcgw-4.0.0 → wcgw-4.1.0}/src/wcgw_cli/openai_utils.py +0 -0
  119. {wcgw-4.0.0 → wcgw-4.1.0}/static/claude-ss.jpg +0 -0
  120. {wcgw-4.0.0 → wcgw-4.1.0}/static/computer-use.jpg +0 -0
  121. {wcgw-4.0.0 → wcgw-4.1.0}/static/example.jpg +0 -0
  122. {wcgw-4.0.0 → wcgw-4.1.0}/static/rocket-icon.png +0 -0
  123. {wcgw-4.0.0 → wcgw-4.1.0}/static/ss1.png +0 -0
  124. {wcgw-4.0.0 → wcgw-4.1.0}/static/workflow-demo.gif +0 -0
  125. {wcgw-4.0.0 → wcgw-4.1.0}/tests/test_file_range_tracking.py +0 -0
  126. {wcgw-4.0.0 → wcgw-4.1.0}/tests/test_mcp_server.py +0 -0
  127. {wcgw-4.0.0 → wcgw-4.1.0}/tests/test_readfiles.py +0 -0
  128. {wcgw-4.0.0 → wcgw-4.1.0}/tests/test_tools.py +0 -0
wcgw-4.1.0/CLAUDE.md ADDED
@@ -0,0 +1,41 @@
1
+ # Alignment instructions to contribute to this repository
2
+
3
+ ## Hard rules
4
+
5
+ - Make sure mypy --strict passes for these two folders `uv run mypy --strict src/wcgw src/wcgw_cli`.
6
+ - Use `list` directly for typing like `list[str]` no need to import `List`. Same thing for `tuple`, `set`, etc.
7
+ - No optional parameters in a function with default values. All parameters must be passed by a caller.
8
+ - This library uses `uv` as package manager. To add a package `uv add numpy`. To run pytest `uv run pytest` and so on.
9
+
10
+ ## Coding mantras
11
+
12
+ ### Reduce states and dependencies between the states
13
+
14
+ - Don't introduce any state unless really necessary.
15
+ - If anything can be derived, avoid storing it or passing it.
16
+
17
+ #### Python `Exception` guideline 1
18
+
19
+ - Exception thrown inside functions are their hidden extra state which should be avoided.
20
+ - Parse don't validate: avoid throwing validation errors by letting the types avoid bad values to be passed in the first place.
21
+
22
+ ### Put burden on type checker not the code reader
23
+
24
+ - No hidden contracts and assumptions.
25
+ - Don't assume any relationship between two states unless it's encoded in the type of the state.
26
+ - Any contract should be enforced by the way types are constructed.
27
+ - If it's just not possible due to complexity to type in such a way to avoid hidden contract, add in docstring details.
28
+
29
+ #### Python `Exception` guideline 2
30
+
31
+ - When you can't avoid it, instead of enforcing the hidden contract as hard failure during runtime, try to return some sensible value instead.
32
+ _Example_
33
+ In PIL adding boxes outside image bounds don't do anything, but they don't fail either, making it a cleaner experience to deal with edge cases.
34
+
35
+ - A functions signature (along with types) should be enough to understand its purpose.
36
+ - This can be achieved by typing the parameters to only take narrow types
37
+
38
+ ### Functions should be as pure as possible
39
+
40
+ - Avoid mutating mutable input parameters, instead return newly derived values in the output and leave upto the caller to update the state if required.
41
+ - It should be clear from function signature what the function computes, this should also enforce the previous point of not updating mutable input parameters.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wcgw
3
- Version: 4.0.0
3
+ Version: 4.1.0
4
4
  Summary: Shell and coding agent on claude and chatgpt
5
5
  Project-URL: Homepage, https://github.com/rusiaaman/wcgw
6
6
  Author-email: Aman Rusia <gapypi@arcfu.com>
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  Empowering chat applications to code, build and run on your local machine.
31
31
 
32
- - Claude - An MCP server on claude desktop for autonomous shell and coding agent. (mac, linux, windows on wsl)
32
+ - Claude - MCP server with tightly integrated shell and code editing tools.
33
33
  - Chatgpt - Allows custom gpt to talk to your shell via a relay server. (linux, mac, windows on wsl)
34
34
 
35
35
  ⚠️ Warning: do not allow BashCommand tool without reviewing the command, it may result in data loss.
@@ -46,6 +46,8 @@ Empowering chat applications to code, build and run on your local machine.
46
46
 
47
47
  ## Updates
48
48
 
49
+ - [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
50
+
49
51
  - [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
50
52
 
51
53
  - [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
@@ -59,7 +61,7 @@ Empowering chat applications to code, build and run on your local machine.
59
61
  ## 🚀 Highlights
60
62
 
61
63
  - ⚡ **Create, Execute, Iterate**: Ask claude to keep running compiler checks till all errors are fixed, or ask it to keep checking for the status of a long running command till it's done.
62
- - ⚡ **Large file edit**: Supports large file incremental edits to avoid token limit issues. Faster than full file write.
64
+ - ⚡ **Large file edit**: Supports large file incremental edits to avoid token limit issues. Smartly selects when to do small edits or large rewrite based on % of change needed.
63
65
  - ⚡ **Syntax checking on edits**: Reports feedback to the LLM if its edits have any syntax errors, so that it can redo it.
64
66
  - ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
65
67
  - ⚡ **File protections**:
@@ -2,7 +2,7 @@
2
2
 
3
3
  Empowering chat applications to code, build and run on your local machine.
4
4
 
5
- - Claude - An MCP server on claude desktop for autonomous shell and coding agent. (mac, linux, windows on wsl)
5
+ - Claude - MCP server with tightly integrated shell and code editing tools.
6
6
  - Chatgpt - Allows custom gpt to talk to your shell via a relay server. (linux, mac, windows on wsl)
7
7
 
8
8
  ⚠️ Warning: do not allow BashCommand tool without reviewing the command, it may result in data loss.
@@ -19,6 +19,8 @@ Empowering chat applications to code, build and run on your local machine.
19
19
 
20
20
  ## Updates
21
21
 
22
+ - [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
23
+
22
24
  - [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
23
25
 
24
26
  - [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
@@ -32,7 +34,7 @@ Empowering chat applications to code, build and run on your local machine.
32
34
  ## 🚀 Highlights
33
35
 
34
36
  - ⚡ **Create, Execute, Iterate**: Ask claude to keep running compiler checks till all errors are fixed, or ask it to keep checking for the status of a long running command till it's done.
35
- - ⚡ **Large file edit**: Supports large file incremental edits to avoid token limit issues. Faster than full file write.
37
+ - ⚡ **Large file edit**: Supports large file incremental edits to avoid token limit issues. Smartly selects when to do small edits or large rewrite based on % of change needed.
36
38
  - ⚡ **Syntax checking on edits**: Reports feedback to the LLM if its edits have any syntax errors, so that it can redo it.
37
39
  - ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
38
40
  - ⚡ **File protections**:
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  authors = [{ name = "Aman Rusia", email = "gapypi@arcfu.com" }]
3
3
  name = "wcgw"
4
- version = "4.0.0"
4
+ version = "4.1.0"
5
5
  description = "Shell and coding agent on claude and chatgpt"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -125,7 +125,6 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
125
125
  session_info = line.split()[0].strip() # e.g., "1234.my_screen"
126
126
  if session_info.endswith(f".{name}"):
127
127
  sessions_to_kill.append(session_info)
128
-
129
128
  # Now, for every session we found, tell screen to quit it.
130
129
  for session in sessions_to_kill:
131
130
  try:
@@ -258,7 +257,12 @@ class BashState:
258
257
 
259
258
  def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
260
259
  self.close_bg_expect_thread()
261
- output = self._shell.expect(pattern, timeout)
260
+ try:
261
+ output = self._shell.expect(pattern, timeout)
262
+ except pexpect.TIMEOUT:
263
+ # Edge case: gets raised when the child fd is not ready in some timeout
264
+ # pexpect/utils.py:143
265
+ return 1
262
266
  return output
263
267
 
264
268
  def send(self, s: str | bytes, set_as_command: Optional[str]) -> int:
@@ -319,11 +323,9 @@ class BashState:
319
323
  self._bg_expect_thread_stop_event = threading.Event()
320
324
 
321
325
  def cleanup(self) -> None:
322
- try:
323
- self.close_bg_expect_thread()
324
- self._shell.close(True)
325
- finally:
326
- cleanup_all_screens_with_name(self._shell_id, self.console)
326
+ cleanup_all_screens_with_name(self._shell_id, self.console)
327
+ self.close_bg_expect_thread()
328
+ self._shell.close(True)
327
329
 
328
330
  def __enter__(self) -> "BashState":
329
331
  return self
@@ -7,7 +7,13 @@ TOLERANCE_TYPES = Literal["SILENT", "WARNING", "ERROR"]
7
7
 
8
8
 
9
9
  class SearchReplaceMatchError(Exception):
10
- pass
10
+ def __init__(self, message: str):
11
+ message = f"""
12
+ {message}
13
+ ---
14
+ Retry immediately with same "percentage_to_change" using search replace blocks fixing above error.
15
+ """
16
+ super().__init__(message)
11
17
 
12
18
 
13
19
  @dataclass
@@ -27,7 +33,9 @@ class TolerancesHit(Tolerance):
27
33
  class FileEditOutput:
28
34
  original_content: list[str]
29
35
  orig_search_blocks: list[list[str]]
30
- edited_with_tolerances: list[tuple[slice, list[TolerancesHit], list[str]]]
36
+ edited_with_tolerances: list[
37
+ tuple[slice, list[TolerancesHit], list[str]]
38
+ ] # Need not be equal to orig_search_blocks when early exit
31
39
 
32
40
  def replace_or_throw(
33
41
  self,
@@ -94,8 +102,7 @@ Error:
94
102
  best_score = hit_score
95
103
  elif abs(hit_score - best_score) < 1e-3:
96
104
  best_hits.append(output)
97
-
98
- return best_hits, best_score < 0
105
+ return best_hits, best_score > 1000
99
106
 
100
107
 
101
108
  def line_process_max_space_tolerance(line: str) -> str:
@@ -205,7 +212,7 @@ class FileEditInput:
205
212
  TolerancesHit(
206
213
  line_process=lambda x: x,
207
214
  severity_cat="ERROR",
208
- score_multiplier=float("-inf"),
215
+ score_multiplier=float("inf"),
209
216
  error_name="The blocks couldn't be matched, maybe the sequence of search blocks was incorrect?",
210
217
  count=max(1, len(search_lines)),
211
218
  )
@@ -241,6 +248,7 @@ class FileEditInput:
241
248
 
242
249
  # search for first block
243
250
  first_block = self.search_replace_blocks[self.search_replace_offset]
251
+ replace_by = first_block[1]
244
252
 
245
253
  # Try exact match
246
254
  matches = match_exact(self.file_lines, self.file_line_offset, first_block[0])
@@ -252,7 +260,6 @@ class FileEditInput:
252
260
  matches_with_tolerances = match_with_tolerance(
253
261
  self.file_lines, self.file_line_offset, first_block[0], self.tolerances
254
262
  )
255
- replace_by = first_block[1]
256
263
  if not matches_with_tolerances:
257
264
  # Try with no empty lines
258
265
  matches_with_tolerances = match_with_tolerance_empty_line(
@@ -278,8 +285,8 @@ class FileEditInput:
278
285
  TolerancesHit(
279
286
  lambda x: x,
280
287
  "ERROR",
281
- -1,
282
- "Couldn't find match. Do you mean to match the lines in the following context?\n```"
288
+ float("inf"),
289
+ "Couldn't find match. Here's the latest snippet from the file which might be relevant for you to consider:\n```"
283
290
  + sim_context
284
291
  + "\n```",
285
292
  int(len(first_block[0]) // sim_sim),
@@ -288,51 +295,40 @@ class FileEditInput:
288
295
  )
289
296
  ]
290
297
 
291
- for match, tolerances in matches_with_tolerances:
292
- if any(
293
- tolerance.error_name == REMOVE_INDENTATION
294
- for tolerance in tolerances
295
- ):
296
- replace_by = fix_indentation(
297
- self.file_lines[match.start : match.stop],
298
- first_block[0],
299
- replace_by,
300
- )
301
-
302
- file_edit_input = FileEditInput(
303
- self.file_lines,
304
- match.stop,
305
- self.search_replace_blocks,
306
- self.search_replace_offset + 1,
307
- self.tolerances,
298
+ else:
299
+ matches_with_tolerances = [(match, []) for match in matches]
300
+
301
+ for match, tolerances in matches_with_tolerances:
302
+ if any(
303
+ tolerance.error_name == REMOVE_INDENTATION for tolerance in tolerances
304
+ ):
305
+ replace_by = fix_indentation(
306
+ self.file_lines[match.start : match.stop],
307
+ first_block[0],
308
+ replace_by,
308
309
  )
309
310
 
310
- remaining_output = file_edit_input.edit_file()
311
- for rem_output in remaining_output:
312
- all_outputs.append(
313
- [
314
- (match, tolerances, replace_by),
315
- *rem_output.edited_with_tolerances,
316
- ]
317
- )
318
- else:
319
- for match in matches:
320
- file_edit_input = FileEditInput(
321
- self.file_lines,
322
- match.stop,
323
- self.search_replace_blocks,
324
- self.search_replace_offset + 1,
325
- self.tolerances,
311
+ file_edit_input = FileEditInput(
312
+ self.file_lines,
313
+ match.stop,
314
+ self.search_replace_blocks,
315
+ self.search_replace_offset + 1,
316
+ self.tolerances,
317
+ )
318
+
319
+ if any(tolerance.severity_cat == "ERROR" for tolerance in tolerances):
320
+ # Exit early
321
+ all_outputs.append(
322
+ [
323
+ (match, tolerances, replace_by),
324
+ ]
326
325
  )
326
+ else:
327
327
  remaining_output = file_edit_input.edit_file()
328
328
  for rem_output in remaining_output:
329
329
  all_outputs.append(
330
330
  [
331
- (
332
- match,
333
- [],
334
- first_block[1],
335
- ),
331
+ (match, tolerances, replace_by),
336
332
  *rem_output.edited_with_tolerances,
337
333
  ]
338
334
  )
@@ -1,5 +1,5 @@
1
1
  import re
2
- from typing import Callable
2
+ from typing import Callable, Optional
3
3
 
4
4
  from .diff_edit import FileEditInput, FileEditOutput, SearchReplaceMatchError
5
5
 
@@ -100,9 +100,10 @@ def search_replace_edit(
100
100
  "No valid search replace blocks found, ensure your SEARCH/REPLACE blocks are formatted correctly"
101
101
  )
102
102
 
103
- edited_content, comments_ = greedy_context_replace(
104
- original_lines, [[x] for x in search_replace_blocks], original_lines, set(), 0
103
+ edited_content, comments_ = edit_with_individual_fallback(
104
+ original_lines, search_replace_blocks
105
105
  )
106
+
106
107
  edited_file = "\n".join(edited_content)
107
108
  if not comments_:
108
109
  comments = "Edited successfully"
@@ -114,63 +115,81 @@ def search_replace_edit(
114
115
  return edited_file, comments
115
116
 
116
117
 
117
- def greedy_context_replace(
118
- original_lines: list[str],
119
- search_replace_blocks: list[list[tuple[list[str], list[str]]]],
120
- running_lines: list[str],
121
- running_comments: set[str],
122
- current_block_offset: int,
123
- ) -> tuple[list[str], set[str]]:
124
- if current_block_offset >= len(search_replace_blocks):
125
- return running_lines, running_comments
126
- current_blocks = search_replace_blocks[current_block_offset]
118
+ def identify_first_differing_block(
119
+ best_matches: list[FileEditOutput],
120
+ ) -> Optional[list[str]]:
121
+ """
122
+ Identify the first search block that differs across multiple best matches.
123
+ Returns the search block content that first shows different matches.
124
+ """
125
+ if not best_matches or len(best_matches) <= 1:
126
+ return None
127
+
128
+ # First, check if the number of blocks differs (shouldn't happen, but let's be safe)
129
+ block_counts = [len(match.edited_with_tolerances) for match in best_matches]
130
+ if not all(count == block_counts[0] for count in block_counts):
131
+ # If block counts differ, just return the first search block as problematic
132
+ return (
133
+ best_matches[0].orig_search_blocks[0]
134
+ if best_matches[0].orig_search_blocks
135
+ else None
136
+ )
137
+
138
+ # Go through each block position and see if the slices differ
139
+ for i in range(min(block_counts)):
140
+ slices = [match.edited_with_tolerances[i][0] for match in best_matches]
127
141
 
128
- outputs = FileEditInput(running_lines, 0, current_blocks, 0).edit_file()
142
+ # Check if we have different slices for this block across matches
143
+ if any(s.start != slices[0].start or s.stop != slices[0].stop for s in slices):
144
+ # We found our differing block - return the search block content
145
+ if i < len(best_matches[0].orig_search_blocks):
146
+ return best_matches[0].orig_search_blocks[i]
147
+ else:
148
+ return None
149
+
150
+ # If we get here, we couldn't identify a specific differing block
151
+ return None
152
+
153
+
154
+ def edit_with_individual_fallback(
155
+ original_lines: list[str], search_replace_blocks: list[tuple[list[str], list[str]]]
156
+ ) -> tuple[list[str], set[str]]:
157
+ outputs = FileEditInput(original_lines, 0, search_replace_blocks, 0).edit_file()
129
158
  best_matches, is_error = FileEditOutput.get_best_match(outputs)
130
159
 
131
- if is_error:
132
- best_matches[0].replace_or_throw(3)
133
- raise Exception("Shouldn't happen")
160
+ try:
161
+ edited_content, comments_ = best_matches[0].replace_or_throw(3)
162
+ except SearchReplaceMatchError:
163
+ if len(search_replace_blocks) > 1:
164
+ # Try one at a time
165
+ all_comments = set[str]()
166
+ running_lines = list(original_lines)
167
+ for block in search_replace_blocks:
168
+ running_lines, comments_ = edit_with_individual_fallback(
169
+ running_lines, [block]
170
+ )
171
+ all_comments |= comments_
172
+ return running_lines, all_comments
173
+ raise
174
+ assert not is_error
134
175
 
135
176
  if len(best_matches) > 1:
136
- # Duplicate found, try to ground using previous blocks.
137
- if current_block_offset == 0:
138
- matches_ = "\n".join(current_blocks[-1][0])
177
+ # Find the first block that differs across matches
178
+ first_diff_block = identify_first_differing_block(best_matches)
179
+ if first_diff_block is not None:
180
+ block_content = "\n".join(first_diff_block)
139
181
  raise SearchReplaceMatchError(f"""
140
- The following block matched more than once:
141
- ---
142
- ```
143
- {matches_}
144
- ```
182
+ The following block matched more than once:
183
+ ```
184
+ {block_content}
185
+ ```
186
+ Consider adding more context before and after this block to make the match unique.
145
187
  """)
146
-
147
188
  else:
148
- search_replace_blocks = (
149
- search_replace_blocks[: current_block_offset - 1]
150
- + [search_replace_blocks[current_block_offset - 1] + current_blocks]
151
- + search_replace_blocks[current_block_offset + 1 :]
152
- )
153
- try:
154
- return greedy_context_replace(
155
- original_lines, search_replace_blocks, original_lines, set(), 0
156
- )
157
- except Exception:
158
- ma_more = "\n".join(current_blocks[-1][0])
159
- raise Exception(f"""
160
- The following block matched more than once:
161
- ---
162
- ```
163
- {ma_more}
164
- ```
165
- """)
166
-
167
- best_match = best_matches[0]
168
- running_lines, comments = best_match.replace_or_throw(3)
169
- running_comments = running_comments | comments
170
- return greedy_context_replace(
171
- original_lines,
172
- search_replace_blocks,
173
- running_lines,
174
- running_comments,
175
- current_block_offset + 1,
176
- )
189
+ raise SearchReplaceMatchError("""
190
+ One of the blocks matched more than once
191
+
192
+ Consider adding more context before and after all the blocks to make the match unique.
193
+ """)
194
+
195
+ return edited_content, comments_
@@ -16,7 +16,7 @@ from wcgw.client.tool_prompts import TOOL_PROMPTS
16
16
  from ...types_ import (
17
17
  Initialize,
18
18
  )
19
- from ..bash_state.bash_state import CONFIG, BashState
19
+ from ..bash_state.bash_state import CONFIG, BashState, get_tmpdir
20
20
  from ..tools import (
21
21
  Context,
22
22
  default_enc,
@@ -155,9 +155,13 @@ async def main() -> None:
155
155
  global BASH_STATE
156
156
  CONFIG.update(3, 55, 5)
157
157
  version = str(importlib.metadata.version("wcgw"))
158
- home_dir = os.path.expanduser("~")
158
+
159
+ # starting_dir is inside tmp dir
160
+ tmp_dir = get_tmpdir()
161
+ starting_dir = os.path.join(tmp_dir, "claude_playground")
162
+
159
163
  with BashState(
160
- Console(), home_dir, None, None, None, None, True, None
164
+ Console(), starting_dir, None, None, None, None, True, None
161
165
  ) as BASH_STATE:
162
166
  BASH_STATE.console.log("wcgw version: " + version)
163
167
  # Run the server using stdin/stdout streams
@@ -125,8 +125,9 @@ Instructions:
125
125
  - Do not install new tools/packages before ensuring no such tools/package or an alternative already exists.
126
126
  - Do not use artifacts if you have access to the repository and not asked by the user to provide artifacts/snippets. Directly create/update using wcgw tools
127
127
  - Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
128
- - Do not use echo to write multi-line files, always use FileEdit tool to update a code.
129
-
128
+ - Do not use echo to write multi-line files, always use FileWriteOrEdit tool to update a code.
129
+ - Provide as many file paths as you need in ReadFiles in one go.
130
+
130
131
  Additional instructions:
131
132
  Always run `pwd` if you get any file or directory not found error to make sure you're not lost, or to get absolute cwd.
132
133
 
@@ -134,18 +135,26 @@ Additional instructions:
134
135
 
135
136
 
136
137
  """
137
- ARCHITECT_PROMPT = """You are now running in "architect" mode. This means
138
+ ARCHITECT_PROMPT = """
139
+ # Instructions
140
+ You are now running in "architect" mode. This means
138
141
  - You are not allowed to edit or update any file. You are not allowed to create any file.
139
142
  - You are not allowed to run any commands that may change disk, system configuration, packages or environment. Only read-only commands are allowed.
140
143
  - Only run commands that allows you to explore the repository, understand the system or read anything of relevance.
141
144
  - Do not use Ctrl-c or interrupt commands without asking the user, because often the programs don't show any update but they still are running.
142
145
  - You are not allowed to change directory (bash will run in -r mode)
143
146
  - Share only snippets when any implementation is requested.
147
+ - Provide as many file paths as you need in ReadFiles in one go.
148
+
149
+ # Disallowed tools (important!)
150
+ - FileWriteOrEdit
144
151
 
152
+ # Response instructions
145
153
  Respond only after doing the following:
146
154
  - Read as many relevant files as possible.
147
155
  - Be comprehensive in your understanding and search of relevant files.
148
156
  - First understand about the project by getting the folder structure (ignoring .git, node_modules, venv, etc.)
157
+ - Share minimal snippets higlighting the changes (avoid large number of lines in the snippets, use ... comments)
149
158
  """
150
159
 
151
160
 
@@ -141,7 +141,22 @@ def get_recent_git_files(repo: Repository, count: int = 10) -> list[str]:
141
141
  return recent_files
142
142
 
143
143
 
144
- def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]:
144
+ def calculate_dynamic_file_limit(total_files: int) -> int:
145
+ # Scale linearly, with minimum and maximum bounds
146
+ min_files = 50
147
+ max_files = 400
148
+
149
+ if total_files <= min_files:
150
+ return min_files
151
+
152
+ scale_factor = (max_files - min_files) / (30000 - min_files)
153
+
154
+ dynamic_limit = min_files + int((total_files - min_files) * scale_factor)
155
+
156
+ return min(max_files, dynamic_limit)
157
+
158
+
159
+ def get_repo_context(file_or_repo_path: str) -> tuple[str, Path]:
145
160
  file_or_repo_path_ = Path(file_or_repo_path).absolute()
146
161
 
147
162
  repo = find_ancestor_with_git(file_or_repo_path_)
@@ -150,9 +165,6 @@ def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]
150
165
  # Determine the context directory
151
166
  if repo is not None:
152
167
  context_dir = Path(repo.path).parent
153
- # Get recent git files - get at least 50 or the max_files count, whichever is larger
154
- recent_files_count = max(10, max_files)
155
- recent_git_files = get_recent_git_files(repo, recent_files_count)
156
168
  else:
157
169
  if file_or_repo_path_.is_file():
158
170
  context_dir = file_or_repo_path_.parent
@@ -162,8 +174,19 @@ def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]
162
174
  # Load workspace stats from the context directory
163
175
  workspace_stats = load_workspace_stats(str(context_dir))
164
176
 
177
+ # Get all files and calculate dynamic max files limit once
165
178
  all_files = get_all_files_max_depth(str(context_dir), 10, repo)
166
179
 
180
+ # For Git repositories, get recent files
181
+ if repo is not None:
182
+ dynamic_max_files = calculate_dynamic_file_limit(len(all_files))
183
+ # Get recent git files - get at least 10 or 20% of dynamic_max_files, whichever is larger
184
+ recent_files_count = max(10, int(dynamic_max_files * 0.2))
185
+ recent_git_files = get_recent_git_files(repo, recent_files_count)
186
+ else:
187
+ # We don't want dynamic limit for non git folders like /tmp or ~
188
+ dynamic_max_files = 50
189
+
167
190
  # Calculate probabilities in batch
168
191
  path_scores = PATH_SCORER.calculate_path_probabilities_batch(all_files)
169
192
 
@@ -218,16 +241,16 @@ def get_repo_context(file_or_repo_path: str, max_files: int) -> tuple[str, Path]
218
241
  if file not in top_files and file in all_files:
219
242
  top_files.append(file)
220
243
 
221
- # Use statistical sorting for the remaining files, but respect max_files limit
244
+ # Use statistical sorting for the remaining files, but respect dynamic_max_files limit
222
245
  # and ensure we don't add duplicates
223
- if len(top_files) < max_files:
246
+ if len(top_files) < dynamic_max_files:
224
247
  # Only add statistically important files that aren't already in top_files
225
248
  for file in sorted_files:
226
- if file not in top_files and len(top_files) < max_files:
249
+ if file not in top_files and len(top_files) < dynamic_max_files:
227
250
  top_files.append(file)
228
251
 
229
- directory_printer = DirectoryTree(context_dir, max_files=max_files)
230
- for file in top_files[:max_files]:
252
+ directory_printer = DirectoryTree(context_dir, max_files=dynamic_max_files)
253
+ for file in top_files[:dynamic_max_files]:
231
254
  directory_printer.expand(file)
232
255
 
233
256
  return directory_printer.display(), context_dir
@@ -245,7 +268,7 @@ if __name__ == "__main__":
245
268
  # Profile using cProfile for overall function statistics
246
269
  profiler = cProfile.Profile()
247
270
  profiler.enable()
248
- result = get_repo_context(folder, 50)[0]
271
+ result = get_repo_context(folder)[0]
249
272
  profiler.disable()
250
273
 
251
274
  # Print cProfile stats
@@ -257,7 +280,7 @@ if __name__ == "__main__":
257
280
  # Profile using line_profiler for line-by-line statistics
258
281
  lp = LineProfiler()
259
282
  lp_wrapper = lp(get_repo_context)
260
- lp_wrapper(folder, 50)
283
+ lp_wrapper(folder)
261
284
 
262
285
  print("\n=== Line-by-line profiling ===")
263
286
  lp.print_stats()
@@ -77,6 +77,7 @@ TOOL_PROMPTS = [
77
77
  description="""
78
78
  - Writes or edits a file based on the percentage of changes.
79
79
  - Use absolute path only (~ allowed).
80
+ - percentage_to_change is calculated as number of existing lines that will have some diff divided by total existing lines.
80
81
  - First write down percentage of lines that need to be replaced in the file (between 0-100) in percentage_to_change
81
82
  - percentage_to_change should be low if mostly new code is to be added. It should be high if a lot of things are to be replaced.
82
83
  - If percentage_to_change > 50, provide full file content in file_content_or_search_replace_blocks