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.
Files changed (77) hide show
  1. hypergolic-0.3.1/LICENSE +21 -0
  2. hypergolic-0.3.1/PKG-INFO +112 -0
  3. hypergolic-0.3.1/README.md +94 -0
  4. hypergolic-0.3.1/hypergolic/__init__.py +3 -0
  5. hypergolic-0.3.1/hypergolic/agents.py +94 -0
  6. hypergolic-0.3.1/hypergolic/app/__init__.py +0 -0
  7. hypergolic-0.3.1/hypergolic/app/constants.py +1 -0
  8. hypergolic-0.3.1/hypergolic/app/crash_logs.py +45 -0
  9. hypergolic-0.3.1/hypergolic/app/entrypoint.py +33 -0
  10. hypergolic-0.3.1/hypergolic/app/lifespan.py +51 -0
  11. hypergolic-0.3.1/hypergolic/app/session_context.py +70 -0
  12. hypergolic-0.3.1/hypergolic/app/version_control.py +407 -0
  13. hypergolic-0.3.1/hypergolic/config.py +31 -0
  14. hypergolic-0.3.1/hypergolic/context.py +9 -0
  15. hypergolic-0.3.1/hypergolic/exploration.py +290 -0
  16. hypergolic-0.3.1/hypergolic/extensions.py +134 -0
  17. hypergolic-0.3.1/hypergolic/prompts/code_reviewer_system_prompt.md +182 -0
  18. hypergolic-0.3.1/hypergolic/prompts/loader.py +59 -0
  19. hypergolic-0.3.1/hypergolic/prompts/resolvers.py +87 -0
  20. hypergolic-0.3.1/hypergolic/prompts/system_prompt.md +82 -0
  21. hypergolic-0.3.1/hypergolic/providers.py +32 -0
  22. hypergolic-0.3.1/hypergolic/tools/__init__.py +0 -0
  23. hypergolic-0.3.1/hypergolic/tools/approval.py +22 -0
  24. hypergolic-0.3.1/hypergolic/tools/cancellation.py +203 -0
  25. hypergolic-0.3.1/hypergolic/tools/code_review/__init__.py +105 -0
  26. hypergolic-0.3.1/hypergolic/tools/code_review/schemas.py +200 -0
  27. hypergolic-0.3.1/hypergolic/tools/command_line.py +63 -0
  28. hypergolic-0.3.1/hypergolic/tools/common.py +18 -0
  29. hypergolic-0.3.1/hypergolic/tools/enums.py +14 -0
  30. hypergolic-0.3.1/hypergolic/tools/file_explorer.py +97 -0
  31. hypergolic-0.3.1/hypergolic/tools/file_operations.py +315 -0
  32. hypergolic-0.3.1/hypergolic/tools/git.py +146 -0
  33. hypergolic-0.3.1/hypergolic/tools/merge_branch.py +45 -0
  34. hypergolic-0.3.1/hypergolic/tools/read_file.py +37 -0
  35. hypergolic-0.3.1/hypergolic/tools/schemas.py +7 -0
  36. hypergolic-0.3.1/hypergolic/tools/screenshot.py +248 -0
  37. hypergolic-0.3.1/hypergolic/tools/search_files.py +121 -0
  38. hypergolic-0.3.1/hypergolic/tools/tool_calls.py +189 -0
  39. hypergolic-0.3.1/hypergolic/tools/tool_list.py +56 -0
  40. hypergolic-0.3.1/hypergolic/tools/window_management.py +311 -0
  41. hypergolic-0.3.1/hypergolic/tui/__init__.py +47 -0
  42. hypergolic-0.3.1/hypergolic/tui/app.py +417 -0
  43. hypergolic-0.3.1/hypergolic/tui/conversation_manager.py +48 -0
  44. hypergolic-0.3.1/hypergolic/tui/session_stats.py +83 -0
  45. hypergolic-0.3.1/hypergolic/tui/streaming.py +78 -0
  46. hypergolic-0.3.1/hypergolic/tui/tool_executor.py +201 -0
  47. hypergolic-0.3.1/hypergolic/tui/tool_ui_handler.py +222 -0
  48. hypergolic-0.3.1/hypergolic/tui/widgets/__init__.py +32 -0
  49. hypergolic-0.3.1/hypergolic/tui/widgets/code_review.py +646 -0
  50. hypergolic-0.3.1/hypergolic/tui/widgets/conversation.py +171 -0
  51. hypergolic-0.3.1/hypergolic/tui/widgets/diff_view.py +324 -0
  52. hypergolic-0.3.1/hypergolic/tui/widgets/header.py +64 -0
  53. hypergolic-0.3.1/hypergolic/tui/widgets/merge_approval.py +210 -0
  54. hypergolic-0.3.1/hypergolic/tui/widgets/prompt_input.py +21 -0
  55. hypergolic-0.3.1/hypergolic/tui/widgets/sidebar.py +229 -0
  56. hypergolic-0.3.1/hypergolic/tui/widgets/tool_approval.py +284 -0
  57. hypergolic-0.3.1/hypergolic/tui/widgets/tool_status.py +186 -0
  58. hypergolic-0.3.1/hypergolic/tui/widgets/tools.py +25 -0
  59. hypergolic-0.3.1/hypergolic.egg-info/PKG-INFO +112 -0
  60. hypergolic-0.3.1/hypergolic.egg-info/SOURCES.txt +75 -0
  61. hypergolic-0.3.1/hypergolic.egg-info/dependency_links.txt +1 -0
  62. hypergolic-0.3.1/hypergolic.egg-info/entry_points.txt +2 -0
  63. hypergolic-0.3.1/hypergolic.egg-info/requires.txt +9 -0
  64. hypergolic-0.3.1/hypergolic.egg-info/top_level.txt +1 -0
  65. hypergolic-0.3.1/pyproject.toml +56 -0
  66. hypergolic-0.3.1/setup.cfg +4 -0
  67. hypergolic-0.3.1/tests/test_cache.py +207 -0
  68. hypergolic-0.3.1/tests/test_cancellation.py +139 -0
  69. hypergolic-0.3.1/tests/test_code_review_widgets.py +151 -0
  70. hypergolic-0.3.1/tests/test_conversation_manager.py +216 -0
  71. hypergolic-0.3.1/tests/test_exploration.py +228 -0
  72. hypergolic-0.3.1/tests/test_extensions.py +107 -0
  73. hypergolic-0.3.1/tests/test_file_operations.py +198 -0
  74. hypergolic-0.3.1/tests/test_interrupt.py +116 -0
  75. hypergolic-0.3.1/tests/test_prompt_loader.py +163 -0
  76. hypergolic-0.3.1/tests/test_session_stats.py +84 -0
  77. hypergolic-0.3.1/tests/test_tool_executor.py +510 -0
@@ -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,3 @@
1
+ from hypergolic.tui.app import HypergolicApp
2
+
3
+ __all__ = ["HypergolicApp"]
@@ -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
+ )