hypergolic 0.3.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.
- hypergolic-0.3.1/LICENSE +21 -0
- hypergolic-0.3.1/PKG-INFO +112 -0
- hypergolic-0.3.1/README.md +94 -0
- hypergolic-0.3.1/hypergolic/__init__.py +3 -0
- hypergolic-0.3.1/hypergolic/agents.py +94 -0
- hypergolic-0.3.1/hypergolic/app/__init__.py +0 -0
- hypergolic-0.3.1/hypergolic/app/constants.py +1 -0
- hypergolic-0.3.1/hypergolic/app/crash_logs.py +45 -0
- hypergolic-0.3.1/hypergolic/app/entrypoint.py +33 -0
- hypergolic-0.3.1/hypergolic/app/lifespan.py +51 -0
- hypergolic-0.3.1/hypergolic/app/session_context.py +70 -0
- hypergolic-0.3.1/hypergolic/app/version_control.py +407 -0
- hypergolic-0.3.1/hypergolic/config.py +31 -0
- hypergolic-0.3.1/hypergolic/context.py +9 -0
- hypergolic-0.3.1/hypergolic/exploration.py +290 -0
- hypergolic-0.3.1/hypergolic/extensions.py +134 -0
- hypergolic-0.3.1/hypergolic/prompts/code_reviewer_system_prompt.md +182 -0
- hypergolic-0.3.1/hypergolic/prompts/loader.py +59 -0
- hypergolic-0.3.1/hypergolic/prompts/resolvers.py +87 -0
- hypergolic-0.3.1/hypergolic/prompts/system_prompt.md +82 -0
- hypergolic-0.3.1/hypergolic/providers.py +32 -0
- hypergolic-0.3.1/hypergolic/tools/__init__.py +0 -0
- hypergolic-0.3.1/hypergolic/tools/approval.py +22 -0
- hypergolic-0.3.1/hypergolic/tools/cancellation.py +203 -0
- hypergolic-0.3.1/hypergolic/tools/code_review/__init__.py +105 -0
- hypergolic-0.3.1/hypergolic/tools/code_review/schemas.py +200 -0
- hypergolic-0.3.1/hypergolic/tools/command_line.py +63 -0
- hypergolic-0.3.1/hypergolic/tools/common.py +18 -0
- hypergolic-0.3.1/hypergolic/tools/enums.py +14 -0
- hypergolic-0.3.1/hypergolic/tools/file_explorer.py +97 -0
- hypergolic-0.3.1/hypergolic/tools/file_operations.py +315 -0
- hypergolic-0.3.1/hypergolic/tools/git.py +146 -0
- hypergolic-0.3.1/hypergolic/tools/merge_branch.py +45 -0
- hypergolic-0.3.1/hypergolic/tools/read_file.py +37 -0
- hypergolic-0.3.1/hypergolic/tools/schemas.py +7 -0
- hypergolic-0.3.1/hypergolic/tools/screenshot.py +248 -0
- hypergolic-0.3.1/hypergolic/tools/search_files.py +121 -0
- hypergolic-0.3.1/hypergolic/tools/tool_calls.py +189 -0
- hypergolic-0.3.1/hypergolic/tools/tool_list.py +56 -0
- hypergolic-0.3.1/hypergolic/tools/window_management.py +311 -0
- hypergolic-0.3.1/hypergolic/tui/__init__.py +47 -0
- hypergolic-0.3.1/hypergolic/tui/app.py +417 -0
- hypergolic-0.3.1/hypergolic/tui/conversation_manager.py +48 -0
- hypergolic-0.3.1/hypergolic/tui/session_stats.py +83 -0
- hypergolic-0.3.1/hypergolic/tui/streaming.py +78 -0
- hypergolic-0.3.1/hypergolic/tui/tool_executor.py +201 -0
- hypergolic-0.3.1/hypergolic/tui/tool_ui_handler.py +222 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/__init__.py +32 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/code_review.py +646 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/conversation.py +171 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/diff_view.py +324 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/header.py +64 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/merge_approval.py +210 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/prompt_input.py +21 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/sidebar.py +229 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/tool_approval.py +284 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/tool_status.py +186 -0
- hypergolic-0.3.1/hypergolic/tui/widgets/tools.py +25 -0
- hypergolic-0.3.1/hypergolic.egg-info/PKG-INFO +112 -0
- hypergolic-0.3.1/hypergolic.egg-info/SOURCES.txt +75 -0
- hypergolic-0.3.1/hypergolic.egg-info/dependency_links.txt +1 -0
- hypergolic-0.3.1/hypergolic.egg-info/entry_points.txt +2 -0
- hypergolic-0.3.1/hypergolic.egg-info/requires.txt +9 -0
- hypergolic-0.3.1/hypergolic.egg-info/top_level.txt +1 -0
- hypergolic-0.3.1/pyproject.toml +56 -0
- hypergolic-0.3.1/setup.cfg +4 -0
- hypergolic-0.3.1/tests/test_cache.py +207 -0
- hypergolic-0.3.1/tests/test_cancellation.py +139 -0
- hypergolic-0.3.1/tests/test_code_review_widgets.py +151 -0
- hypergolic-0.3.1/tests/test_conversation_manager.py +216 -0
- hypergolic-0.3.1/tests/test_exploration.py +228 -0
- hypergolic-0.3.1/tests/test_extensions.py +107 -0
- hypergolic-0.3.1/tests/test_file_operations.py +198 -0
- hypergolic-0.3.1/tests/test_interrupt.py +116 -0
- hypergolic-0.3.1/tests/test_prompt_loader.py +163 -0
- hypergolic-0.3.1/tests/test_session_stats.py +84 -0
- hypergolic-0.3.1/tests/test_tool_executor.py +510 -0
hypergolic-0.3.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Townley
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hypergolic
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: ==3.14.2
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: anthropic==0.76.0
|
|
9
|
+
Requires-Dist: jinja2>=3.1.6
|
|
10
|
+
Requires-Dist: pydantic>=2.12.5
|
|
11
|
+
Requires-Dist: pyobjc-framework-applicationservices>=12.1
|
|
12
|
+
Requires-Dist: pyobjc-framework-quartz>=12.1
|
|
13
|
+
Requires-Dist: requests>=2.32.5
|
|
14
|
+
Requires-Dist: textual>=7.3.0
|
|
15
|
+
Requires-Dist: tiktoken>=0.12.0
|
|
16
|
+
Requires-Dist: typer>=0.21.1
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Hypergolic
|
|
20
|
+
|
|
21
|
+
A powerful AI coding assistant with command-line tool access for macOS.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Execute shell commands on macOS
|
|
26
|
+
- Read and write files with surgical precision
|
|
27
|
+
- Navigate directories and explore projects
|
|
28
|
+
- Screenshot capture for visual context
|
|
29
|
+
- Code review integration for quality assurance
|
|
30
|
+
- Git worktree isolation for safe code modifications
|
|
31
|
+
- Multi-layered prompt system for personalized behavior
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### 1. Configure Environment Variables
|
|
36
|
+
|
|
37
|
+
Hypergolic requires three environment variables to connect to your LLM provider:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export HYPERGOLIC_API_KEY="your-api-key"
|
|
41
|
+
export HYPERGOLIC_BASE_URL="https://api.anthropic.com"
|
|
42
|
+
export HYPERGOLIC_MODEL="claude-sonnet-4-20250514"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Add these to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) for persistence.
|
|
46
|
+
|
|
47
|
+
### 2. Install with uv
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uv tool install hypergolic
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This installs `h` as a globally available command.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Navigate to any git repository and run:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
h
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This launches an interactive TUI where you can chat with the AI assistant. The assistant can:
|
|
64
|
+
|
|
65
|
+
- Read and modify files in your project
|
|
66
|
+
- Run shell commands
|
|
67
|
+
- Search through codebases
|
|
68
|
+
- Take screenshots for visual debugging
|
|
69
|
+
- Commit changes and request code reviews
|
|
70
|
+
|
|
71
|
+
### Workflow
|
|
72
|
+
|
|
73
|
+
1. **Start a session** — Run `h` from your project directory
|
|
74
|
+
2. **Describe your task** — The assistant will explore your codebase and implement changes
|
|
75
|
+
3. **Review changes** — The assistant works in an isolated git worktree, keeping your working directory clean
|
|
76
|
+
4. **Merge when ready** — After code review, changes merge back to your original branch
|
|
77
|
+
|
|
78
|
+
## Getting Started Tips
|
|
79
|
+
|
|
80
|
+
### Be Specific
|
|
81
|
+
The more context you provide, the better the results. Instead of "fix the bug", try "the login form submits twice when clicking the button rapidly — add debouncing".
|
|
82
|
+
|
|
83
|
+
### Let It Explore
|
|
84
|
+
The assistant works best when it can read existing code before making changes. If it asks to explore your codebase first, let it.
|
|
85
|
+
|
|
86
|
+
### Use Screenshots
|
|
87
|
+
For UI issues, the assistant can take screenshots. Just mention "take a screenshot" or describe what you're seeing visually.
|
|
88
|
+
|
|
89
|
+
### Customize Behavior
|
|
90
|
+
Create `~/.hypergolic/user_prompt.md` for personal preferences that apply to all projects, or `.agents/project_prompt.md` in your repo for project-specific instructions.
|
|
91
|
+
|
|
92
|
+
### Trust the Worktree
|
|
93
|
+
All changes happen in an isolated git worktree. Your working directory stays untouched until you explicitly merge. Feel free to experiment.
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
Hypergolic uses an ephemeral git worktree system for safe code modifications. Each session gets its own isolated branch, allowing the AI to make changes without affecting your working directory. After code review, changes can be merged back into the original branch.
|
|
98
|
+
|
|
99
|
+
## Built With
|
|
100
|
+
|
|
101
|
+
- Python 3.14+
|
|
102
|
+
- [Anthropic Claude API](https://www.anthropic.com/)
|
|
103
|
+
- [Textual](https://textual.textualize.io/) for the TUI
|
|
104
|
+
- [uv](https://docs.astral.sh/uv/) for dependency management
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
MIT License - See LICENSE file for details.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Hypergolic
|
|
2
|
+
|
|
3
|
+
A powerful AI coding assistant with command-line tool access for macOS.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Execute shell commands on macOS
|
|
8
|
+
- Read and write files with surgical precision
|
|
9
|
+
- Navigate directories and explore projects
|
|
10
|
+
- Screenshot capture for visual context
|
|
11
|
+
- Code review integration for quality assurance
|
|
12
|
+
- Git worktree isolation for safe code modifications
|
|
13
|
+
- Multi-layered prompt system for personalized behavior
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### 1. Configure Environment Variables
|
|
18
|
+
|
|
19
|
+
Hypergolic requires three environment variables to connect to your LLM provider:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export HYPERGOLIC_API_KEY="your-api-key"
|
|
23
|
+
export HYPERGOLIC_BASE_URL="https://api.anthropic.com"
|
|
24
|
+
export HYPERGOLIC_MODEL="claude-sonnet-4-20250514"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Add these to your shell profile (`~/.zshrc`, `~/.bashrc`, etc.) for persistence.
|
|
28
|
+
|
|
29
|
+
### 2. Install with uv
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv tool install hypergolic
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
This installs `h` as a globally available command.
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
Navigate to any git repository and run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
h
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This launches an interactive TUI where you can chat with the AI assistant. The assistant can:
|
|
46
|
+
|
|
47
|
+
- Read and modify files in your project
|
|
48
|
+
- Run shell commands
|
|
49
|
+
- Search through codebases
|
|
50
|
+
- Take screenshots for visual debugging
|
|
51
|
+
- Commit changes and request code reviews
|
|
52
|
+
|
|
53
|
+
### Workflow
|
|
54
|
+
|
|
55
|
+
1. **Start a session** — Run `h` from your project directory
|
|
56
|
+
2. **Describe your task** — The assistant will explore your codebase and implement changes
|
|
57
|
+
3. **Review changes** — The assistant works in an isolated git worktree, keeping your working directory clean
|
|
58
|
+
4. **Merge when ready** — After code review, changes merge back to your original branch
|
|
59
|
+
|
|
60
|
+
## Getting Started Tips
|
|
61
|
+
|
|
62
|
+
### Be Specific
|
|
63
|
+
The more context you provide, the better the results. Instead of "fix the bug", try "the login form submits twice when clicking the button rapidly — add debouncing".
|
|
64
|
+
|
|
65
|
+
### Let It Explore
|
|
66
|
+
The assistant works best when it can read existing code before making changes. If it asks to explore your codebase first, let it.
|
|
67
|
+
|
|
68
|
+
### Use Screenshots
|
|
69
|
+
For UI issues, the assistant can take screenshots. Just mention "take a screenshot" or describe what you're seeing visually.
|
|
70
|
+
|
|
71
|
+
### Customize Behavior
|
|
72
|
+
Create `~/.hypergolic/user_prompt.md` for personal preferences that apply to all projects, or `.agents/project_prompt.md` in your repo for project-specific instructions.
|
|
73
|
+
|
|
74
|
+
### Trust the Worktree
|
|
75
|
+
All changes happen in an isolated git worktree. Your working directory stays untouched until you explicitly merge. Feel free to experiment.
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
Hypergolic uses an ephemeral git worktree system for safe code modifications. Each session gets its own isolated branch, allowing the AI to make changes without affecting your working directory. After code review, changes can be merged back into the original branch.
|
|
80
|
+
|
|
81
|
+
## Built With
|
|
82
|
+
|
|
83
|
+
- Python 3.14+
|
|
84
|
+
- [Anthropic Claude API](https://www.anthropic.com/)
|
|
85
|
+
- [Textual](https://textual.textualize.io/) for the TUI
|
|
86
|
+
- [uv](https://docs.astral.sh/uv/) for dependency management
|
|
87
|
+
|
|
88
|
+
## Contributing
|
|
89
|
+
|
|
90
|
+
Contributions are welcome! Please open an issue or submit a pull request.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT License - See LICENSE file for details.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from anthropic.types import MessageParam, ToolResultBlockParam, ToolUseBlock
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def find_tool_use_ids_in_message(message: MessageParam) -> set[str]:
|
|
7
|
+
tool_ids: set[str] = set()
|
|
8
|
+
content = message.get("content", [])
|
|
9
|
+
|
|
10
|
+
if not isinstance(content, list):
|
|
11
|
+
return tool_ids
|
|
12
|
+
|
|
13
|
+
for block in content:
|
|
14
|
+
if isinstance(block, dict) and block.get("type") == "tool_use":
|
|
15
|
+
tool_id = block.get("id")
|
|
16
|
+
if tool_id:
|
|
17
|
+
tool_ids.add(tool_id)
|
|
18
|
+
elif isinstance(block, ToolUseBlock):
|
|
19
|
+
tool_ids.add(block.id)
|
|
20
|
+
|
|
21
|
+
return tool_ids
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_tool_result_ids_in_message(message: MessageParam) -> set[str]:
|
|
25
|
+
result_ids: set[str] = set()
|
|
26
|
+
content = message.get("content", [])
|
|
27
|
+
|
|
28
|
+
if not isinstance(content, list):
|
|
29
|
+
return result_ids
|
|
30
|
+
|
|
31
|
+
for block in content:
|
|
32
|
+
if isinstance(block, dict) and block.get("type") == "tool_result":
|
|
33
|
+
tool_use_id = block.get("tool_use_id")
|
|
34
|
+
if tool_use_id:
|
|
35
|
+
result_ids.add(tool_use_id)
|
|
36
|
+
|
|
37
|
+
return result_ids
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_incomplete_tool_uses(messages: list[MessageParam]) -> set[str]:
|
|
41
|
+
all_tool_use_ids: set[str] = set()
|
|
42
|
+
all_tool_result_ids: set[str] = set()
|
|
43
|
+
|
|
44
|
+
for message in messages:
|
|
45
|
+
role = message.get("role")
|
|
46
|
+
if role == "assistant":
|
|
47
|
+
all_tool_use_ids.update(find_tool_use_ids_in_message(message))
|
|
48
|
+
elif role == "user":
|
|
49
|
+
all_tool_result_ids.update(find_tool_result_ids_in_message(message))
|
|
50
|
+
|
|
51
|
+
return all_tool_use_ids - all_tool_result_ids
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def create_interrupt_tool_results(
|
|
55
|
+
incomplete_tool_ids: set[str],
|
|
56
|
+
) -> list[ToolResultBlockParam]:
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
"type": "tool_result",
|
|
60
|
+
"tool_use_id": tool_id,
|
|
61
|
+
"content": "Tool execution was interrupted by user",
|
|
62
|
+
"is_error": True,
|
|
63
|
+
}
|
|
64
|
+
for tool_id in incomplete_tool_ids
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create_interrupt_user_message(user_text: str) -> MessageParam:
|
|
69
|
+
interrupt_notice = (
|
|
70
|
+
"[USER INTERRUPT] The user has interrupted your previous response. "
|
|
71
|
+
"Any tool calls that were in progress have been cancelled. "
|
|
72
|
+
"Please acknowledge the interruption and address the user's new message:\n\n"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"role": "user",
|
|
77
|
+
"content": [{"type": "text", "text": interrupt_notice + user_text}],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def prepare_interrupted_history(
|
|
82
|
+
messages: list[MessageParam],
|
|
83
|
+
interrupt_message: str,
|
|
84
|
+
) -> list[MessageParam]:
|
|
85
|
+
result: list[MessageParam] = list(messages)
|
|
86
|
+
incomplete_ids = find_incomplete_tool_uses(result)
|
|
87
|
+
|
|
88
|
+
if incomplete_ids:
|
|
89
|
+
error_results = create_interrupt_tool_results(incomplete_ids)
|
|
90
|
+
error_message = cast(MessageParam, {"role": "user", "content": error_results})
|
|
91
|
+
result.append(error_message)
|
|
92
|
+
|
|
93
|
+
result.append(create_interrupt_user_message(interrupt_message))
|
|
94
|
+
return result
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SESSION_WORKTREE_PREFIX = ".agent-worktree-"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import traceback
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def save_crash_log(exc_type, exc_value, exc_tb) -> None:
|
|
8
|
+
"""Save crash information to the error log file.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
exc_type: The exception type.
|
|
12
|
+
exc_value: The exception instance.
|
|
13
|
+
exc_tb: The traceback object.
|
|
14
|
+
"""
|
|
15
|
+
log_path = get_crash_log_path()
|
|
16
|
+
|
|
17
|
+
# Ensure the log directory exists
|
|
18
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
|
|
20
|
+
# Format the crash log entry
|
|
21
|
+
timestamp = datetime.now().isoformat()
|
|
22
|
+
tb_lines = traceback.format_exception(exc_type, exc_value, exc_tb)
|
|
23
|
+
tb_str = "".join(tb_lines)
|
|
24
|
+
|
|
25
|
+
crash_entry = f"""
|
|
26
|
+
================================================================================
|
|
27
|
+
CRASH REPORT: {timestamp}
|
|
28
|
+
================================================================================
|
|
29
|
+
Exception Type: {exc_type.__name__}
|
|
30
|
+
Exception Message: {exc_value}
|
|
31
|
+
|
|
32
|
+
Traceback:
|
|
33
|
+
{tb_str}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Append to the log file
|
|
37
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
38
|
+
f.write(crash_entry)
|
|
39
|
+
|
|
40
|
+
print(f"\nCrash logged to: {log_path}", file=sys.stderr)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_crash_log_path() -> Path:
|
|
44
|
+
"""Get the path to the crash log file."""
|
|
45
|
+
return Path.home() / ".hypergolic" / "logs" / "error.log"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from hypergolic.app.crash_logs import save_crash_log
|
|
4
|
+
from hypergolic.app.lifespan import HypergolicLifespan
|
|
5
|
+
from hypergolic.app.session_context import build_session_context
|
|
6
|
+
from hypergolic.config import HypergolicConfig
|
|
7
|
+
from hypergolic.tui.app import HypergolicApp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
try:
|
|
12
|
+
session_context = build_session_context()
|
|
13
|
+
config = HypergolicConfig()
|
|
14
|
+
|
|
15
|
+
with HypergolicLifespan(session_context):
|
|
16
|
+
app = HypergolicApp(session_context, config)
|
|
17
|
+
app.run()
|
|
18
|
+
except KeyboardInterrupt:
|
|
19
|
+
# Normal exit via Ctrl+C, don't log as crash
|
|
20
|
+
sys.exit(0)
|
|
21
|
+
except Exception:
|
|
22
|
+
# Log the crash (but don't let logging failures mask the original error)
|
|
23
|
+
try:
|
|
24
|
+
save_crash_log(*sys.exc_info())
|
|
25
|
+
except Exception:
|
|
26
|
+
pass # Silently ignore logging failures
|
|
27
|
+
|
|
28
|
+
# Re-raise so the user sees the error and gets appropriate exit code
|
|
29
|
+
raise
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
if __name__ == "__main__":
|
|
33
|
+
main()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
3
|
+
from hypergolic.app.session_context import SessionContext
|
|
4
|
+
from hypergolic.app.version_control import (
|
|
5
|
+
BranchCleanupResult,
|
|
6
|
+
cleanup_agent_branch,
|
|
7
|
+
create_agent_branch,
|
|
8
|
+
create_worktree,
|
|
9
|
+
remove_worktree,
|
|
10
|
+
stash_dirty_branch,
|
|
11
|
+
unstash_dirty_branch,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@contextmanager
|
|
16
|
+
def HypergolicLifespan(session_context: SessionContext, merge_on_exit: bool = False):
|
|
17
|
+
start(session_context)
|
|
18
|
+
try:
|
|
19
|
+
yield
|
|
20
|
+
finally:
|
|
21
|
+
end(session_context, merge_on_exit=merge_on_exit)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def start(session_context: SessionContext):
|
|
25
|
+
print("Starting session...")
|
|
26
|
+
stash_dirty_branch(session_context)
|
|
27
|
+
create_agent_branch(session_context)
|
|
28
|
+
create_worktree(session_context)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def end(session_context: SessionContext, merge_on_exit: bool = False):
|
|
32
|
+
print("Ending session...")
|
|
33
|
+
remove_worktree(session_context)
|
|
34
|
+
|
|
35
|
+
result = cleanup_agent_branch(session_context, merge_if_changes=merge_on_exit)
|
|
36
|
+
|
|
37
|
+
match result:
|
|
38
|
+
case BranchCleanupResult.DELETED:
|
|
39
|
+
print(f"Cleaned up empty branch: {session_context.agent_branch}")
|
|
40
|
+
case BranchCleanupResult.MERGED:
|
|
41
|
+
print(
|
|
42
|
+
f"✅ Merged {session_context.agent_branch} into {session_context.original_branch}"
|
|
43
|
+
)
|
|
44
|
+
case BranchCleanupResult.PRESERVED:
|
|
45
|
+
print(f"⚠️ Branch preserved with changes: {session_context.agent_branch}")
|
|
46
|
+
print(f" To merge: git merge {session_context.agent_branch}")
|
|
47
|
+
print(f" To delete: git branch -D {session_context.agent_branch}")
|
|
48
|
+
case BranchCleanupResult.NOT_FOUND:
|
|
49
|
+
pass # Branch was already cleaned up somehow
|
|
50
|
+
|
|
51
|
+
unstash_dirty_branch(session_context)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import uuid
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from hypergolic.app.constants import SESSION_WORKTREE_PREFIX
|
|
8
|
+
from hypergolic.tools.common import perform_command
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SessionContext(BaseModel):
|
|
12
|
+
agent_branch: str
|
|
13
|
+
base_commit: str
|
|
14
|
+
original_branch: str
|
|
15
|
+
cwd: Path
|
|
16
|
+
branch_dirty: bool
|
|
17
|
+
git_root: Path
|
|
18
|
+
worktree_path: Path
|
|
19
|
+
project_name: str
|
|
20
|
+
remote_url: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def build_session_context() -> SessionContext:
|
|
24
|
+
original_branch = perform_command(["git", "branch", "--show-current"])
|
|
25
|
+
git_root = perform_command(["git", "rev-parse", "--show-toplevel"])
|
|
26
|
+
git_root_path = Path(git_root)
|
|
27
|
+
session_id = uuid.uuid4().hex[:8]
|
|
28
|
+
agent_branch = f"agent/session-{session_id}"
|
|
29
|
+
project_name = git_root_path.name
|
|
30
|
+
worktree_path = (
|
|
31
|
+
git_root_path.parent / f"{SESSION_WORKTREE_PREFIX}{project_name}-{session_id}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
staged_changes = run_subprocess(["git", "diff", "--cached", "--quiet"])
|
|
35
|
+
unstaged_changes = run_subprocess(["git", "diff", "--quiet"])
|
|
36
|
+
base_commit = perform_command(["git", "rev-parse", "HEAD"])
|
|
37
|
+
branch_dirty = staged_changes.returncode != 0 or unstaged_changes.returncode != 0
|
|
38
|
+
|
|
39
|
+
remote_url = get_remote_url()
|
|
40
|
+
|
|
41
|
+
return SessionContext(
|
|
42
|
+
agent_branch=agent_branch,
|
|
43
|
+
base_commit=base_commit,
|
|
44
|
+
branch_dirty=branch_dirty,
|
|
45
|
+
original_branch=original_branch,
|
|
46
|
+
cwd=Path.cwd(),
|
|
47
|
+
git_root=Path(git_root),
|
|
48
|
+
worktree_path=worktree_path,
|
|
49
|
+
project_name=project_name,
|
|
50
|
+
remote_url=remote_url,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_remote_url() -> str | None:
|
|
55
|
+
result = run_subprocess(["git", "remote", "get-url", "origin"])
|
|
56
|
+
if result.returncode == 0:
|
|
57
|
+
url = result.stdout.strip()
|
|
58
|
+
if url:
|
|
59
|
+
return url
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def run_subprocess(parts: list[str]) -> subprocess.CompletedProcess:
|
|
64
|
+
return subprocess.run(
|
|
65
|
+
parts,
|
|
66
|
+
cwd=Path.cwd(),
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
check=False,
|
|
70
|
+
)
|