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.
- wcgw-4.1.1/CLAUDE.md +41 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/PKG-INFO +6 -3
- {wcgw-4.0.0 → wcgw-4.1.1}/README.md +4 -2
- {wcgw-4.0.0 → wcgw-4.1.1}/pyproject.toml +5 -1
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/bash_state/bash_state.py +143 -10
- wcgw-4.1.1/src/wcgw/client/bash_state/parser/__init__.py +7 -0
- wcgw-4.1.1/src/wcgw/client/bash_state/parser/bash_statement_parser.py +181 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/file_ops/diff_edit.py +42 -46
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/file_ops/search_replace.py +74 -55
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/mcp_server/server.py +7 -3
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/modes.py +12 -3
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/repo_context.py +34 -11
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/tool_prompts.py +1 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/tools.py +17 -1
- wcgw-4.1.1/tests/test_bash_parser.py +80 -0
- wcgw-4.1.1/tests/test_bash_parser_complex.py +84 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_edit.py +122 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_tools.py +18 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/uv.lock +77 -1
- {wcgw-4.0.0 → wcgw-4.1.1}/.github/workflows/python-publish.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/.github/workflows/python-tests.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/.github/workflows/python-types.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/.gitignore +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/.gitmodules +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/.python-version +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/.vscode/settings.json +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/Dockerfile +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/LICENSE +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/gpt_action_json_schema.json +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/gpt_instructions.txt +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/openai.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.git +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/main-checks.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/publish-pypi.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/pull-request-checks.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.github/workflows/shared.yml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.gitignore +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/.python-version +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/CODE_OF_CONDUCT.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/CONTRIBUTING.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/LICENSE +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/README.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/RELEASE.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/SECURITY.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/README.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/.python-version +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/README.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/__main__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/mcp_simple_prompt/server.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-prompt/pyproject.toml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/.python-version +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/README.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/__main__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/mcp_simple_resource/server.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-resource/pyproject.toml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/.python-version +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/README.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/__main__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/mcp_simple_tool/server.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/examples/servers/simple-tool/pyproject.toml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/pyproject.toml +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/__main__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/session.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/sse.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/client/stdio.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/py.typed +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/__main__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/models.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/session.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/sse.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/stdio.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/server/websocket.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/context.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/exceptions.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/memory.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/progress.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/session.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/shared/version.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/src/mcp_wcgw/types.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/client/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/client/test_session.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/client/test_stdio.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/conftest.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/server/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/server/test_session.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/server/test_stdio.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/shared/test_memory.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/tests/test_types.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/mcp_wcgw_fork/uv.lock +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/common.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/diff-instructions.txt +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/encoder/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/mcp_server/Readme.md +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/mcp_server/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/memory.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/display_tree.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/file_stats.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/path_prob.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/paths_model.vocab +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/client/repo_ops/paths_tokens.model +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/py.typed +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/relay/client.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/relay/serve.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/relay/static/privacy.txt +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw/types_.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/__init__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/__main__.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/anthropic_client.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/cli.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/openai_client.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/src/wcgw_cli/openai_utils.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/static/claude-ss.jpg +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/static/computer-use.jpg +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/static/example.jpg +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/static/rocket-icon.png +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/static/ss1.png +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/static/workflow-demo.gif +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_file_range_tracking.py +0 -0
- {wcgw-4.0.0 → wcgw-4.1.1}/tests/test_mcp_server.py +0 -0
- {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.
|
|
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 -
|
|
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.
|
|
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 -
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
820
|
-
|
|
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,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()
|