wcgw 4.0.0__tar.gz → 4.1.1__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 (132) hide show
  1. wcgw-4.1.1/CLAUDE.md +41 -0
  2. {wcgw-4.0.0 → wcgw-4.1.1}/PKG-INFO +6 -3
  3. {wcgw-4.0.0 → wcgw-4.1.1}/README.md +4 -2
  4. {wcgw-4.0.0 → wcgw-4.1.1}/pyproject.toml +5 -1
  5. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/bash_state/bash_state.py +143 -10
  6. wcgw-4.1.1/src/wcgw/client/bash_state/parser/__init__.py +7 -0
  7. wcgw-4.1.1/src/wcgw/client/bash_state/parser/bash_statement_parser.py +181 -0
  8. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/file_ops/diff_edit.py +42 -46
  9. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/file_ops/search_replace.py +74 -55
  10. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/mcp_server/server.py +7 -3
  11. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/modes.py +12 -3
  12. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/repo_context.py +34 -11
  13. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/tool_prompts.py +1 -0
  14. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/tools.py +17 -1
  15. wcgw-4.1.1/tests/test_bash_parser.py +80 -0
  16. wcgw-4.1.1/tests/test_bash_parser_complex.py +84 -0
  17. {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_edit.py +122 -0
  18. {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_tools.py +18 -0
  19. {wcgw-4.0.0 → wcgw-4.1.1}/uv.lock +77 -1
  20. {wcgw-4.0.0 → wcgw-4.1.1}/.github/workflows/python-publish.yml +0 -0
  21. {wcgw-4.0.0 → wcgw-4.1.1}/.github/workflows/python-tests.yml +0 -0
  22. {wcgw-4.0.0 → wcgw-4.1.1}/.github/workflows/python-types.yml +0 -0
  23. {wcgw-4.0.0 → wcgw-4.1.1}/.gitignore +0 -0
  24. {wcgw-4.0.0 → wcgw-4.1.1}/.gitmodules +0 -0
  25. {wcgw-4.0.0 → wcgw-4.1.1}/.python-version +0 -0
  26. {wcgw-4.0.0 → wcgw-4.1.1}/.vscode/settings.json +0 -0
  27. {wcgw-4.0.0 → wcgw-4.1.1}/Dockerfile +0 -0
  28. {wcgw-4.0.0 → wcgw-4.1.1}/LICENSE +0 -0
  29. {wcgw-4.0.0 → wcgw-4.1.1}/gpt_action_json_schema.json +0 -0
  30. {wcgw-4.0.0 → wcgw-4.1.1}/gpt_instructions.txt +0 -0
  31. {wcgw-4.0.0 → wcgw-4.1.1}/openai.md +0 -0
  32. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.git +0 -0
  33. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  34. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  35. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
  36. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
  37. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
  38. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
  39. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.gitignore +0 -0
  40. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.python-version +0 -0
  41. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
  42. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
  43. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/LICENSE +0 -0
  44. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/README.md +0 -0
  45. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/RELEASE.md +0 -0
  46. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/SECURITY.md +0 -0
  47. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/README.md +0 -0
  48. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
  49. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
  50. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
  51. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
  52. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
  53. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
  54. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
  55. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
  56. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
  57. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
  58. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
  59. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
  60. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
  61. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
  62. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
  63. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
  64. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
  65. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
  66. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/pyproject.toml +0 -0
  67. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
  68. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
  69. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
  70. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
  71. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
  72. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
  73. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
  74. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
  75. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
  76. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
  77. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
  78. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
  79. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
  80. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
  81. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
  82. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
  83. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
  84. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
  85. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
  86. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
  87. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
  88. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
  89. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
  90. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
  91. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
  92. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
  93. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
  94. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
  95. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
  96. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
  97. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
  98. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
  99. {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/uv.lock +0 -0
  100. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/__init__.py +0 -0
  101. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/__init__.py +0 -0
  102. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/common.py +0 -0
  103. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/diff-instructions.txt +0 -0
  104. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/encoder/__init__.py +0 -0
  105. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/mcp_server/Readme.md +0 -0
  106. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/mcp_server/__init__.py +0 -0
  107. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/memory.py +0 -0
  108. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/display_tree.py +0 -0
  109. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/file_stats.py +0 -0
  110. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/path_prob.py +0 -0
  111. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
  112. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
  113. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/py.typed +0 -0
  114. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/relay/client.py +0 -0
  115. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/relay/serve.py +0 -0
  116. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/relay/static/privacy.txt +0 -0
  117. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/types_.py +0 -0
  118. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/__init__.py +0 -0
  119. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/__main__.py +0 -0
  120. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/anthropic_client.py +0 -0
  121. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/cli.py +0 -0
  122. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/openai_client.py +0 -0
  123. {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/openai_utils.py +0 -0
  124. {wcgw-4.0.0 → wcgw-4.1.1}/static/claude-ss.jpg +0 -0
  125. {wcgw-4.0.0 → wcgw-4.1.1}/static/computer-use.jpg +0 -0
  126. {wcgw-4.0.0 → wcgw-4.1.1}/static/example.jpg +0 -0
  127. {wcgw-4.0.0 → wcgw-4.1.1}/static/rocket-icon.png +0 -0
  128. {wcgw-4.0.0 → wcgw-4.1.1}/static/ss1.png +0 -0
  129. {wcgw-4.0.0 → wcgw-4.1.1}/static/workflow-demo.gif +0 -0
  130. {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_file_range_tracking.py +0 -0
  131. {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_mcp_server.py +0 -0
  132. {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_readfiles.py +0 -0
wcgw-4.1.1/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.1
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>
@@ -11,6 +11,7 @@ Requires-Dist: fastapi>=0.115.0
11
11
  Requires-Dist: openai>=1.46.0
12
12
  Requires-Dist: petname>=2.6
13
13
  Requires-Dist: pexpect>=4.9.0
14
+ Requires-Dist: psutil>=7.0.0
14
15
  Requires-Dist: pydantic>=2.9.2
15
16
  Requires-Dist: pygit2>=1.16.0
16
17
  Requires-Dist: pyte>=0.8.2
@@ -29,7 +30,7 @@ Description-Content-Type: text/markdown
29
30
 
30
31
  Empowering chat applications to code, build and run on your local machine.
31
32
 
32
- - Claude - An MCP server on claude desktop for autonomous shell and coding agent. (mac, linux, windows on wsl)
33
+ - Claude - MCP server with tightly integrated shell and code editing tools.
33
34
  - Chatgpt - Allows custom gpt to talk to your shell via a relay server. (linux, mac, windows on wsl)
34
35
 
35
36
  ⚠️ Warning: do not allow BashCommand tool without reviewing the command, it may result in data loss.
@@ -46,6 +47,8 @@ Empowering chat applications to code, build and run on your local machine.
46
47
 
47
48
  ## Updates
48
49
 
50
+ - [24 Mar 2025] Improved writing and editing experience for sonnet 3.7, CLAUDE.md gets loaded automatically.
51
+
49
52
  - [16 Feb 2025] You can now attach to the working terminal that the AI uses. See the "attach-to-terminal" section below.
50
53
 
51
54
  - [15 Jan 2025] Modes introduced: architect, code-writer, and all powerful wcgw mode.
@@ -59,7 +62,7 @@ Empowering chat applications to code, build and run on your local machine.
59
62
  ## 🚀 Highlights
60
63
 
61
64
  - ⚡ **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.
65
+ - ⚡ **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
66
  - ⚡ **Syntax checking on edits**: Reports feedback to the LLM if its edits have any syntax errors, so that it can redo it.
64
67
  - ⚡ **Interactive Command Handling**: Supports interactive commands using arrow keys, interrupt, and ansi escape sequences.
65
68
  - ⚡ **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.1"
5
5
  description = "Shell and coding agent on claude and chatgpt"
6
6
  readme = "README.md"
7
7
  requires-python = ">=3.11"
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "tokenizers>=0.21.0",
24
24
  "pygit2>=1.16.0",
25
25
  "syntax-checker>=0.3.0",
26
+ "psutil>=7.0.0",
26
27
  ]
27
28
 
28
29
  [project.urls]
@@ -58,6 +59,9 @@ dev-dependencies = [
58
59
  "line-profiler>=4.2.0",
59
60
  "pytest-asyncio>=0.25.3",
60
61
  "types-pexpect>=4.9.0.20241208",
62
+ "types-psutil>=7.0.0.20250218",
63
+ "tree-sitter>=0.24.0",
64
+ "tree-sitter-bash>=0.23.3",
61
65
  ]
62
66
 
63
67
  [tool.pytest.ini_options]
@@ -16,6 +16,7 @@ from typing import (
16
16
  )
17
17
 
18
18
  import pexpect
19
+ import psutil
19
20
  import pyte
20
21
 
21
22
  from ...types_ import (
@@ -30,6 +31,7 @@ from ...types_ import (
30
31
  )
31
32
  from ..encoder import EncoderDecoder
32
33
  from ..modes import BashCommandMode, FileEditMode, WriteIfEmptyMode
34
+ from .parser.bash_statement_parser import BashStatementParser
33
35
 
34
36
  PROMPT_CONST = "wcgw→" + " "
35
37
  PROMPT_STATEMENT = "export GIT_PAGER=cat PAGER=cat PROMPT_COMMAND= PS1='wcgw→'' '"
@@ -87,6 +89,116 @@ def check_if_screen_command_available() -> bool:
87
89
  return False
88
90
 
89
91
 
92
+ def get_wcgw_screen_sessions() -> list[str]:
93
+ """
94
+ Get a list of all WCGW screen session IDs.
95
+
96
+ Returns:
97
+ List of screen session IDs that match the wcgw pattern.
98
+ """
99
+ screen_sessions = []
100
+
101
+ try:
102
+ # Get list of all screen sessions
103
+ result = subprocess.run(
104
+ ["screen", "-ls"],
105
+ capture_output=True,
106
+ text=True,
107
+ check=False, # Don't raise exception on non-zero exit code
108
+ timeout=0.5,
109
+ )
110
+ output = result.stdout or result.stderr or ""
111
+
112
+ # Parse screen output to get session IDs
113
+ for line in output.splitlines():
114
+ line = line.strip()
115
+ if not line or not line[0].isdigit():
116
+ continue
117
+
118
+ # Extract session info (e.g., "1234.wcgw.123456 (Detached)")
119
+ session_parts = line.split()
120
+ if not session_parts:
121
+ continue
122
+
123
+ session_id = session_parts[0].strip()
124
+
125
+ # Check if it's a WCGW session
126
+ if ".wcgw." in session_id:
127
+ screen_sessions.append(session_id)
128
+ except Exception:
129
+ # If anything goes wrong, just return empty list
130
+ pass
131
+
132
+ return screen_sessions
133
+
134
+
135
+ def get_orphaned_wcgw_screens() -> list[str]:
136
+ """
137
+ Identify orphaned WCGW screen sessions where the parent process has PID 1
138
+ or doesn't exist.
139
+
140
+ Returns:
141
+ List of screen session IDs that are orphaned and match the wcgw pattern.
142
+ """
143
+ orphaned_screens = []
144
+
145
+ try:
146
+ # Get list of all WCGW screen sessions
147
+ screen_sessions = get_wcgw_screen_sessions()
148
+
149
+ for session_id in screen_sessions:
150
+ # Extract PID from session ID (first part before the dot)
151
+ try:
152
+ pid = int(session_id.split(".")[0])
153
+
154
+ # Check if process exists and if its parent is PID 1
155
+ try:
156
+ process = psutil.Process(pid)
157
+ parent_pid = process.ppid()
158
+
159
+ if parent_pid == 1:
160
+ # This is an orphaned process
161
+ orphaned_screens.append(session_id)
162
+ except psutil.NoSuchProcess:
163
+ # Process doesn't exist anymore, consider it orphaned
164
+ orphaned_screens.append(session_id)
165
+ except (ValueError, IndexError):
166
+ # Couldn't parse PID, skip
167
+ continue
168
+ except Exception:
169
+ # If anything goes wrong, just return empty list
170
+ pass
171
+
172
+ return orphaned_screens
173
+
174
+
175
+ def cleanup_orphaned_wcgw_screens(console: Console) -> None:
176
+ """
177
+ Clean up all orphaned WCGW screen sessions.
178
+
179
+ Args:
180
+ console: Console for logging.
181
+ """
182
+ orphaned_sessions = get_orphaned_wcgw_screens()
183
+
184
+ if not orphaned_sessions:
185
+ return
186
+
187
+ console.log(
188
+ f"Found {len(orphaned_sessions)} orphaned WCGW screen sessions to clean up"
189
+ )
190
+
191
+ for session in orphaned_sessions:
192
+ try:
193
+ subprocess.run(
194
+ ["screen", "-S", session, "-X", "quit"],
195
+ check=False,
196
+ timeout=CONFIG.timeout,
197
+ )
198
+ except Exception as e:
199
+ console.log(f"Failed to kill orphaned screen session: {session}\n{e}")
200
+
201
+
90
202
  def cleanup_all_screens_with_name(name: str, console: Console) -> None:
91
203
  """
92
204
  There could be in worst case multiple screens with same name, clear them if any.
@@ -125,7 +237,6 @@ def cleanup_all_screens_with_name(name: str, console: Console) -> None:
125
237
  session_info = line.split()[0].strip() # e.g., "1234.my_screen"
126
238
  if session_info.endswith(f".{name}"):
127
239
  sessions_to_kill.append(session_info)
128
-
129
240
  # Now, for every session we found, tell screen to quit it.
130
241
  for session in sessions_to_kill:
131
242
  try:
@@ -258,13 +369,20 @@ class BashState:
258
369
 
259
370
  def expect(self, pattern: Any, timeout: Optional[float] = -1) -> int:
260
371
  self.close_bg_expect_thread()
261
- output = self._shell.expect(pattern, timeout)
372
+ try:
373
+ output = self._shell.expect(pattern, timeout)
374
+ except pexpect.TIMEOUT:
375
+ # Edge case: gets raised when the child fd is not ready in some timeout
376
+ # pexpect/utils.py:143
377
+ return 1
262
378
  return output
263
379
 
264
380
  def send(self, s: str | bytes, set_as_command: Optional[str]) -> int:
265
381
  self.close_bg_expect_thread()
266
382
  if set_as_command is not None:
267
383
  self._last_command = set_as_command
384
+ # if s == "\n":
385
+ # return self._shell.sendcontrol("m")
268
386
  output = self._shell.send(s)
269
387
  return output
270
388
 
@@ -319,11 +437,9 @@ class BashState:
319
437
  self._bg_expect_thread_stop_event = threading.Event()
320
438
 
321
439
  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)
440
+ cleanup_all_screens_with_name(self._shell_id, self.console)
441
+ self.close_bg_expect_thread()
442
+ self._shell.close(True)
327
443
 
328
444
  def __enter__(self) -> "BashState":
329
445
  return self
@@ -385,6 +501,11 @@ class BashState:
385
501
  self._last_command = ""
386
502
  # Ensure self._cwd exists
387
503
  os.makedirs(self._cwd, exist_ok=True)
504
+
505
+ # Clean up orphaned WCGW screen sessions
506
+ if check_if_screen_command_available():
507
+ cleanup_orphaned_wcgw_screens(self.console)
508
+
388
509
  try:
389
510
  self._shell, self._shell_id = start_shell(
390
511
  self._bash_command_mode.bash_mode == "restricted_mode",
@@ -815,10 +936,22 @@ def _execute_bash(
815
936
  raise ValueError(WAITING_INPUT_MESSAGE)
816
937
 
817
938
  command = command_data.command.strip()
939
+
940
+ # Check for multiple statements using the bash statement parser
818
941
  if "\n" in command:
819
- raise ValueError(
820
- "Command should not contain newline character in middle. Run only one command at a time."
821
- )
942
+ try:
943
+ parser = BashStatementParser()
944
+ statements = parser.parse_string(command)
945
+ if len(statements) > 1:
946
+ return (
947
+ "Error: Command contains multiple statements. Please run only one bash statement at a time.",
948
+ 0.0,
949
+ )
950
+ except Exception:
951
+ # Fall back to simple newline check if something goes wrong
952
+ raise ValueError(
953
+ "Command should not contain newline character in middle. Run only one command at a time."
954
+ )
822
955
 
823
956
  for i in range(0, len(command), 128):
824
957
  bash_state.send(command[i : i + 128], set_as_command=None)
@@ -0,0 +1,7 @@
1
+ """
2
+ Parser for bash statements using tree-sitter.
3
+
4
+ This module provides functionality to parse and identify individual bash statements.
5
+ """
6
+
7
+ from .bash_statement_parser import BashStatementParser, Statement
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bash Statement Parser
4
+
5
+ This script parses bash scripts and identifies individual statements using tree-sitter.
6
+ It correctly handles multi-line strings, command chains with && and ||, and semicolon-separated statements.
7
+ """
8
+
9
+ import sys
10
+ from dataclasses import dataclass
11
+ from typing import Any, List, Optional
12
+
13
+ import tree_sitter_bash
14
+ from tree_sitter import Language, Parser
15
+
16
+
17
+ @dataclass
18
+ class Statement:
19
+ """A bash statement with its source code and position information."""
20
+
21
+ text: str
22
+ start_line: int
23
+ end_line: int
24
+ start_byte: int
25
+ end_byte: int
26
+ node_type: str
27
+ parent_type: Optional[str] = None
28
+
29
+ def __str__(self) -> str:
30
+ return self.text.strip()
31
+
32
+
33
+ class BashStatementParser:
34
+ def __init__(self) -> None:
35
+ # Use the precompiled bash language
36
+ self.language = Language(tree_sitter_bash.language())
37
+ self.parser = Parser(self.language)
38
+
39
+ def parse_file(self, file_path: str) -> List[Statement]:
40
+ """Parse a bash script file and return a list of statements."""
41
+ with open(file_path, "r", encoding="utf-8") as f:
42
+ content = f.read()
43
+ return self.parse_string(content)
44
+
45
+ def parse_string(self, content: str) -> List[Statement]:
46
+ """Parse a string containing bash script and return a list of statements."""
47
+ tree = self.parser.parse(bytes(content, "utf-8"))
48
+ root_node = tree.root_node
49
+
50
+ # For debugging: Uncomment to print the tree structure
51
+ # self._print_tree(root_node, content)
52
+
53
+ statements: List[Statement] = []
54
+ self._extract_statements(root_node, content, statements, None)
55
+
56
+ # Post-process statements to handle multi-line statements correctly
57
+ return self._post_process_statements(statements, content)
58
+
59
+ def _print_tree(self, node: Any, content: str, indent: str = "") -> None:
60
+ """Debug helper to print the entire syntax tree."""
61
+ node_text = content[node.start_byte : node.end_byte]
62
+ if len(node_text) > 40:
63
+ node_text = node_text[:37] + "..."
64
+ print(f"{indent}{node.type}: {repr(node_text)}")
65
+ for child in node.children:
66
+ self._print_tree(child, content, indent + " ")
67
+
68
+ def _extract_statements(
69
+ self,
70
+ node: Any,
71
+ content: str,
72
+ statements: List[Statement],
73
+ parent_type: Optional[str],
74
+ ) -> None:
75
+ """Recursively extract statements from the syntax tree."""
76
+ # Node types that represent bash statements
77
+ statement_node_types = {
78
+ # Basic statements
79
+ "command",
80
+ "variable_assignment",
81
+ "declaration_command",
82
+ "unset_command",
83
+ # Control flow statements
84
+ "for_statement",
85
+ "c_style_for_statement",
86
+ "while_statement",
87
+ "if_statement",
88
+ "case_statement",
89
+ # Function definition
90
+ "function_definition",
91
+ # Command chains and groups
92
+ "pipeline", # For command chains with | and |&
93
+ "list", # For command chains with && and ||
94
+ "compound_statement",
95
+ "subshell",
96
+ "redirected_statement",
97
+ }
98
+
99
+ # Create a Statement object for this node if it's a recognized statement type
100
+ if node.type in statement_node_types:
101
+ # Get the text of this statement
102
+ start_byte = node.start_byte
103
+ end_byte = node.end_byte
104
+ statement_text = content[start_byte:end_byte]
105
+
106
+ # Get line numbers
107
+ start_line = (
108
+ node.start_point[0] + 1
109
+ ) # tree-sitter uses 0-indexed line numbers
110
+ end_line = node.end_point[0] + 1
111
+
112
+ statements.append(
113
+ Statement(
114
+ text=statement_text,
115
+ start_line=start_line,
116
+ end_line=end_line,
117
+ start_byte=start_byte,
118
+ end_byte=end_byte,
119
+ node_type=node.type,
120
+ parent_type=parent_type,
121
+ )
122
+ )
123
+
124
+ # Update parent type for children
125
+ parent_type = node.type
126
+
127
+ # Recursively process all children
128
+ for child in node.children:
129
+ self._extract_statements(child, content, statements, parent_type)
130
+
131
+ def _post_process_statements(
132
+ self, statements: List[Statement], content: str
133
+ ) -> List[Statement]:
134
+ if not statements:
135
+ return []
136
+
137
+ # Filter out list statements that have been split
138
+ top_statements = []
139
+ for stmt in statements:
140
+ # Skip statements that are contained within others
141
+ is_contained = False
142
+ for other in statements:
143
+ if other is stmt:
144
+ continue
145
+
146
+ # Check if completely contained (except for lists we've split)
147
+ if other.node_type != "list" or ";" not in other.text:
148
+ if (
149
+ other.start_line <= stmt.start_line
150
+ and other.end_line >= stmt.end_line
151
+ and len(other.text) > len(stmt.text)
152
+ and stmt.text in other.text
153
+ ):
154
+ is_contained = True
155
+ break
156
+
157
+ if not is_contained:
158
+ top_statements.append(stmt)
159
+
160
+ # Sort by position in file for consistent output
161
+ top_statements.sort(key=lambda s: (s.start_line, s.text))
162
+
163
+ return top_statements
164
+
165
+
166
+ def main() -> None:
167
+ if len(sys.argv) < 2:
168
+ print("Usage: python bash_statement_parser.py <bash_script_file>")
169
+ sys.exit(1)
170
+
171
+ parser = BashStatementParser()
172
+ statements = parser.parse_file(sys.argv[1])
173
+
174
+ print(f"Found {len(statements)} statements:")
175
+ for i, stmt in enumerate(statements, 1):
176
+ print(f"\n--- Statement {i} (Lines {stmt.start_line}-{stmt.end_line}) ---")
177
+ print(stmt)
178
+
179
+
180
+ if __name__ == "__main__":
181
+ main()