hire-ai 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hire_ai-0.1.0/.gitignore +75 -0
- hire_ai-0.1.0/LICENSE +21 -0
- hire_ai-0.1.0/PKG-INFO +125 -0
- hire_ai-0.1.0/README.md +94 -0
- hire_ai-0.1.0/hire/__init__.py +3 -0
- hire_ai-0.1.0/hire/adapters/__init__.py +21 -0
- hire_ai-0.1.0/hire/adapters/base.py +42 -0
- hire_ai-0.1.0/hire/adapters/claude.py +74 -0
- hire_ai-0.1.0/hire/adapters/codex.py +98 -0
- hire_ai-0.1.0/hire/adapters/gemini.py +91 -0
- hire_ai-0.1.0/hire/cli.py +164 -0
- hire_ai-0.1.0/hire/commands/__init__.py +8 -0
- hire_ai-0.1.0/hire/commands/ask.py +143 -0
- hire_ai-0.1.0/hire/commands/delete.py +35 -0
- hire_ai-0.1.0/hire/commands/sessions.py +38 -0
- hire_ai-0.1.0/hire/commands/show.py +31 -0
- hire_ai-0.1.0/hire/config.py +48 -0
- hire_ai-0.1.0/hire/paths.py +53 -0
- hire_ai-0.1.0/hire/session.py +163 -0
- hire_ai-0.1.0/pyproject.toml +66 -0
hire_ai-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
.Python
|
|
8
|
+
build/
|
|
9
|
+
develop-eggs/
|
|
10
|
+
dist/
|
|
11
|
+
downloads/
|
|
12
|
+
eggs/
|
|
13
|
+
.eggs/
|
|
14
|
+
lib/
|
|
15
|
+
lib64/
|
|
16
|
+
parts/
|
|
17
|
+
sdist/
|
|
18
|
+
var/
|
|
19
|
+
wheels/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
.installed.cfg
|
|
22
|
+
*.egg
|
|
23
|
+
|
|
24
|
+
# PyInstaller
|
|
25
|
+
*.manifest
|
|
26
|
+
*.spec
|
|
27
|
+
|
|
28
|
+
# Installer logs
|
|
29
|
+
pip-log.txt
|
|
30
|
+
pip-delete-this-directory.txt
|
|
31
|
+
|
|
32
|
+
# Unit test / coverage reports
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
.coverage
|
|
37
|
+
.coverage.*
|
|
38
|
+
.cache
|
|
39
|
+
nosetests.xml
|
|
40
|
+
coverage.xml
|
|
41
|
+
*.cover
|
|
42
|
+
*.py,cover
|
|
43
|
+
.hypothesis/
|
|
44
|
+
.pytest_cache/
|
|
45
|
+
|
|
46
|
+
# Translations
|
|
47
|
+
*.mo
|
|
48
|
+
*.pot
|
|
49
|
+
|
|
50
|
+
# Environments
|
|
51
|
+
.env
|
|
52
|
+
.venv
|
|
53
|
+
env/
|
|
54
|
+
venv/
|
|
55
|
+
ENV/
|
|
56
|
+
env.bak/
|
|
57
|
+
venv.bak/
|
|
58
|
+
|
|
59
|
+
# mypy
|
|
60
|
+
.mypy_cache/
|
|
61
|
+
.dmypy.json
|
|
62
|
+
dmypy.json
|
|
63
|
+
|
|
64
|
+
# ruff
|
|
65
|
+
.ruff_cache/
|
|
66
|
+
|
|
67
|
+
# IDE
|
|
68
|
+
.idea/
|
|
69
|
+
.vscode/
|
|
70
|
+
*.swp
|
|
71
|
+
*.swo
|
|
72
|
+
*~
|
|
73
|
+
|
|
74
|
+
# macOS
|
|
75
|
+
.DS_Store
|
hire_ai-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 nichiki
|
|
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.
|
hire_ai-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hire-ai
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI to orchestrate AI agents (Claude, Codex, Gemini)
|
|
5
|
+
Project-URL: Homepage, https://github.com/nichiki/hire
|
|
6
|
+
Project-URL: Repository, https://github.com/nichiki/hire
|
|
7
|
+
Project-URL: Issues, https://github.com/nichiki/hire/issues
|
|
8
|
+
Author: nichiki
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,ai,claude,cli,codex,gemini,llm
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
29
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# hire
|
|
33
|
+
|
|
34
|
+
CLI to orchestrate AI agents (Claude, Codex, Gemini).
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Using pipx (recommended)
|
|
40
|
+
pipx install hire
|
|
41
|
+
|
|
42
|
+
# Using pip
|
|
43
|
+
pip install hire
|
|
44
|
+
|
|
45
|
+
# Using Homebrew
|
|
46
|
+
brew install nichiki/tap/hire
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Prerequisites
|
|
50
|
+
|
|
51
|
+
You need at least one of the following CLI tools installed:
|
|
52
|
+
|
|
53
|
+
- [Claude CLI](https://docs.anthropic.com/claude-code)
|
|
54
|
+
- [Codex CLI](https://github.com/openai/codex)
|
|
55
|
+
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Basic usage - hire an agent
|
|
61
|
+
hire codex "Design a REST API for a todo app"
|
|
62
|
+
hire gemini "Research the latest React 19 features"
|
|
63
|
+
hire claude "Review this code for security issues"
|
|
64
|
+
|
|
65
|
+
# Continue a session
|
|
66
|
+
hire -c codex "Tell me more about the authentication"
|
|
67
|
+
hire -s SESSION_ID "Follow up question"
|
|
68
|
+
|
|
69
|
+
# Name a session for later
|
|
70
|
+
hire -n my-project codex "Start designing the architecture"
|
|
71
|
+
hire -s my-project "What about the database schema?"
|
|
72
|
+
|
|
73
|
+
# Output as JSON
|
|
74
|
+
hire gemini "Summarize this" --json
|
|
75
|
+
|
|
76
|
+
# Session management
|
|
77
|
+
hire sessions # List all sessions
|
|
78
|
+
hire sessions codex # List Codex sessions only
|
|
79
|
+
hire show SESSION_ID # Show session details
|
|
80
|
+
hire delete SESSION_ID # Delete a session
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Options
|
|
84
|
+
|
|
85
|
+
| Option | Description |
|
|
86
|
+
|--------|-------------|
|
|
87
|
+
| `-c, --continue` | Continue the latest session |
|
|
88
|
+
| `-s, --session ID` | Continue a specific session |
|
|
89
|
+
| `-n, --name NAME` | Name the session |
|
|
90
|
+
| `-m, --model MODEL` | Specify model to use |
|
|
91
|
+
| `--json` | Output in JSON format |
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Config is stored at `~/.config/hire/config.json`:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"adapters": {
|
|
100
|
+
"claude": {
|
|
101
|
+
"command": "claude",
|
|
102
|
+
"args": ["--dangerously-skip-permissions"]
|
|
103
|
+
},
|
|
104
|
+
"codex": {
|
|
105
|
+
"command": "codex",
|
|
106
|
+
"args": ["--full-auto"]
|
|
107
|
+
},
|
|
108
|
+
"gemini": {
|
|
109
|
+
"command": "gemini",
|
|
110
|
+
"args": ["-y"]
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"defaults": {
|
|
114
|
+
"agent": "claude"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Data Storage
|
|
120
|
+
|
|
121
|
+
Sessions are stored at `~/.local/share/hire/sessions/`.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
hire_ai-0.1.0/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# hire
|
|
2
|
+
|
|
3
|
+
CLI to orchestrate AI agents (Claude, Codex, Gemini).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Using pipx (recommended)
|
|
9
|
+
pipx install hire
|
|
10
|
+
|
|
11
|
+
# Using pip
|
|
12
|
+
pip install hire
|
|
13
|
+
|
|
14
|
+
# Using Homebrew
|
|
15
|
+
brew install nichiki/tap/hire
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Prerequisites
|
|
19
|
+
|
|
20
|
+
You need at least one of the following CLI tools installed:
|
|
21
|
+
|
|
22
|
+
- [Claude CLI](https://docs.anthropic.com/claude-code)
|
|
23
|
+
- [Codex CLI](https://github.com/openai/codex)
|
|
24
|
+
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Basic usage - hire an agent
|
|
30
|
+
hire codex "Design a REST API for a todo app"
|
|
31
|
+
hire gemini "Research the latest React 19 features"
|
|
32
|
+
hire claude "Review this code for security issues"
|
|
33
|
+
|
|
34
|
+
# Continue a session
|
|
35
|
+
hire -c codex "Tell me more about the authentication"
|
|
36
|
+
hire -s SESSION_ID "Follow up question"
|
|
37
|
+
|
|
38
|
+
# Name a session for later
|
|
39
|
+
hire -n my-project codex "Start designing the architecture"
|
|
40
|
+
hire -s my-project "What about the database schema?"
|
|
41
|
+
|
|
42
|
+
# Output as JSON
|
|
43
|
+
hire gemini "Summarize this" --json
|
|
44
|
+
|
|
45
|
+
# Session management
|
|
46
|
+
hire sessions # List all sessions
|
|
47
|
+
hire sessions codex # List Codex sessions only
|
|
48
|
+
hire show SESSION_ID # Show session details
|
|
49
|
+
hire delete SESSION_ID # Delete a session
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
| Option | Description |
|
|
55
|
+
|--------|-------------|
|
|
56
|
+
| `-c, --continue` | Continue the latest session |
|
|
57
|
+
| `-s, --session ID` | Continue a specific session |
|
|
58
|
+
| `-n, --name NAME` | Name the session |
|
|
59
|
+
| `-m, --model MODEL` | Specify model to use |
|
|
60
|
+
| `--json` | Output in JSON format |
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Config is stored at `~/.config/hire/config.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"adapters": {
|
|
69
|
+
"claude": {
|
|
70
|
+
"command": "claude",
|
|
71
|
+
"args": ["--dangerously-skip-permissions"]
|
|
72
|
+
},
|
|
73
|
+
"codex": {
|
|
74
|
+
"command": "codex",
|
|
75
|
+
"args": ["--full-auto"]
|
|
76
|
+
},
|
|
77
|
+
"gemini": {
|
|
78
|
+
"command": "gemini",
|
|
79
|
+
"args": ["-y"]
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"defaults": {
|
|
83
|
+
"agent": "claude"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Data Storage
|
|
89
|
+
|
|
90
|
+
Sessions are stored at `~/.local/share/hire/sessions/`.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Agent adapters."""
|
|
2
|
+
|
|
3
|
+
from .base import AgentAdapter
|
|
4
|
+
from .claude import ClaudeAdapter
|
|
5
|
+
from .codex import CodexAdapter
|
|
6
|
+
from .gemini import GeminiAdapter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_adapter(agent: str) -> AgentAdapter:
|
|
10
|
+
"""Get an adapter for the specified agent."""
|
|
11
|
+
adapters = {
|
|
12
|
+
"claude": ClaudeAdapter,
|
|
13
|
+
"codex": CodexAdapter,
|
|
14
|
+
"gemini": GeminiAdapter,
|
|
15
|
+
}
|
|
16
|
+
if agent not in adapters:
|
|
17
|
+
raise ValueError(f"Unknown agent: {agent}. Available: {list(adapters.keys())}")
|
|
18
|
+
return adapters[agent]()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = ["AgentAdapter", "ClaudeAdapter", "CodexAdapter", "GeminiAdapter", "get_adapter"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Base adapter class."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AgentAdapter(ABC):
|
|
8
|
+
"""Abstract base class for agent adapters."""
|
|
9
|
+
|
|
10
|
+
name: str = "base"
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def ask(
|
|
14
|
+
self,
|
|
15
|
+
message: str,
|
|
16
|
+
session_id: str | None = None,
|
|
17
|
+
model: str | None = None,
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""
|
|
20
|
+
Send a message to the agent and get a response.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
message: The message to send
|
|
24
|
+
session_id: Optional CLI session ID for continuation
|
|
25
|
+
model: Optional model to use
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
dict with keys:
|
|
29
|
+
- response: The agent's response text
|
|
30
|
+
- session_id: The CLI session ID for future continuation
|
|
31
|
+
- raw: The raw output from the CLI (for debugging)
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def build_command(
|
|
36
|
+
self,
|
|
37
|
+
message: str,
|
|
38
|
+
session_id: str | None = None,
|
|
39
|
+
model: str | None = None,
|
|
40
|
+
) -> list[str]:
|
|
41
|
+
"""Build the command to execute. Override in subclasses."""
|
|
42
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Claude CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..config import get_adapter_config
|
|
8
|
+
from .base import AgentAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClaudeAdapter(AgentAdapter):
|
|
12
|
+
"""Adapter for Claude CLI."""
|
|
13
|
+
|
|
14
|
+
name = "claude"
|
|
15
|
+
|
|
16
|
+
def build_command(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
session_id: str | None = None,
|
|
20
|
+
model: str | None = None,
|
|
21
|
+
) -> list[str]:
|
|
22
|
+
"""Build the claude command."""
|
|
23
|
+
config = get_adapter_config("claude")
|
|
24
|
+
command = config.get("command", "claude")
|
|
25
|
+
args = config.get("args", [])
|
|
26
|
+
|
|
27
|
+
cmd = [command, "-p", message, "--output-format", "json"]
|
|
28
|
+
cmd.extend(args)
|
|
29
|
+
|
|
30
|
+
if session_id:
|
|
31
|
+
cmd.extend(["--resume", session_id])
|
|
32
|
+
|
|
33
|
+
if model:
|
|
34
|
+
cmd.extend(["--model", model])
|
|
35
|
+
|
|
36
|
+
return cmd
|
|
37
|
+
|
|
38
|
+
def ask(
|
|
39
|
+
self,
|
|
40
|
+
message: str,
|
|
41
|
+
session_id: str | None = None,
|
|
42
|
+
model: str | None = None,
|
|
43
|
+
) -> dict[str, Any]:
|
|
44
|
+
"""Send a message to Claude and get a response."""
|
|
45
|
+
cmd = self.build_command(message, session_id, model)
|
|
46
|
+
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
cmd,
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if result.returncode != 0:
|
|
54
|
+
return {
|
|
55
|
+
"response": None,
|
|
56
|
+
"session_id": session_id,
|
|
57
|
+
"error": result.stderr or "Command failed",
|
|
58
|
+
"raw": result.stdout,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
data = json.loads(result.stdout)
|
|
63
|
+
return {
|
|
64
|
+
"response": data.get("result", ""),
|
|
65
|
+
"session_id": data.get("session_id", session_id),
|
|
66
|
+
"raw": data,
|
|
67
|
+
}
|
|
68
|
+
except json.JSONDecodeError:
|
|
69
|
+
# If not JSON, return raw output
|
|
70
|
+
return {
|
|
71
|
+
"response": result.stdout,
|
|
72
|
+
"session_id": session_id,
|
|
73
|
+
"raw": result.stdout,
|
|
74
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Codex CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..config import get_adapter_config
|
|
8
|
+
from .base import AgentAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CodexAdapter(AgentAdapter):
|
|
12
|
+
"""Adapter for Codex CLI."""
|
|
13
|
+
|
|
14
|
+
name = "codex"
|
|
15
|
+
|
|
16
|
+
def build_command(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
session_id: str | None = None,
|
|
20
|
+
model: str | None = None,
|
|
21
|
+
) -> list[str]:
|
|
22
|
+
"""Build the codex command."""
|
|
23
|
+
config = get_adapter_config("codex")
|
|
24
|
+
command = config.get("command", "codex")
|
|
25
|
+
args = config.get("args", [])
|
|
26
|
+
|
|
27
|
+
if session_id:
|
|
28
|
+
# Resume session: codex exec resume <SESSION_ID> "message"
|
|
29
|
+
cmd = [command, "exec", "resume", session_id, message, "--json", "--skip-git-repo-check"]
|
|
30
|
+
else:
|
|
31
|
+
# New session: codex exec "message"
|
|
32
|
+
cmd = [command, "exec", message, "--json", "--skip-git-repo-check"]
|
|
33
|
+
|
|
34
|
+
cmd.extend(args)
|
|
35
|
+
|
|
36
|
+
if model:
|
|
37
|
+
cmd.extend(["--model", model])
|
|
38
|
+
|
|
39
|
+
return cmd
|
|
40
|
+
|
|
41
|
+
def ask(
|
|
42
|
+
self,
|
|
43
|
+
message: str,
|
|
44
|
+
session_id: str | None = None,
|
|
45
|
+
model: str | None = None,
|
|
46
|
+
) -> dict[str, Any]:
|
|
47
|
+
"""Send a message to Codex and get a response."""
|
|
48
|
+
cmd = self.build_command(message, session_id, model)
|
|
49
|
+
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
cmd,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if result.returncode != 0:
|
|
57
|
+
return {
|
|
58
|
+
"response": None,
|
|
59
|
+
"session_id": session_id,
|
|
60
|
+
"error": result.stderr or "Command failed",
|
|
61
|
+
"raw": result.stdout,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Codex outputs JSONL (one JSON object per line)
|
|
65
|
+
# Parse all lines and extract the final response
|
|
66
|
+
lines = result.stdout.strip().split("\n")
|
|
67
|
+
response_text = ""
|
|
68
|
+
new_session_id = session_id
|
|
69
|
+
|
|
70
|
+
for line in lines:
|
|
71
|
+
if not line.strip():
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
event = json.loads(line)
|
|
75
|
+
event_type = event.get("type", "")
|
|
76
|
+
|
|
77
|
+
# Get thread_id from thread.started event
|
|
78
|
+
if event_type == "thread.started":
|
|
79
|
+
new_session_id = event.get("thread_id", new_session_id)
|
|
80
|
+
|
|
81
|
+
# Get response text from item.completed with agent_message
|
|
82
|
+
if event_type == "item.completed":
|
|
83
|
+
item = event.get("item", {})
|
|
84
|
+
if item.get("type") == "agent_message":
|
|
85
|
+
response_text = item.get("text", "")
|
|
86
|
+
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# If no structured response found, use raw output
|
|
91
|
+
if not response_text:
|
|
92
|
+
response_text = result.stdout.strip()
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
"response": response_text,
|
|
96
|
+
"session_id": new_session_id,
|
|
97
|
+
"raw": result.stdout,
|
|
98
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Gemini CLI adapter."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..config import get_adapter_config
|
|
8
|
+
from .base import AgentAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GeminiAdapter(AgentAdapter):
|
|
12
|
+
"""Adapter for Gemini CLI."""
|
|
13
|
+
|
|
14
|
+
name = "gemini"
|
|
15
|
+
|
|
16
|
+
def build_command(
|
|
17
|
+
self,
|
|
18
|
+
message: str,
|
|
19
|
+
session_id: str | None = None,
|
|
20
|
+
model: str | None = None,
|
|
21
|
+
) -> list[str]:
|
|
22
|
+
"""Build the gemini command."""
|
|
23
|
+
config = get_adapter_config("gemini")
|
|
24
|
+
command = config.get("command", "gemini")
|
|
25
|
+
args = config.get("args", [])
|
|
26
|
+
|
|
27
|
+
# gemini -p "message" -o json -y
|
|
28
|
+
cmd = [command, "-p", message, "-o", "json"]
|
|
29
|
+
cmd.extend(args)
|
|
30
|
+
|
|
31
|
+
# Resume uses "latest" or index number, not session ID
|
|
32
|
+
if session_id:
|
|
33
|
+
cmd.extend(["-r", session_id])
|
|
34
|
+
|
|
35
|
+
if model:
|
|
36
|
+
cmd.extend(["-m", model])
|
|
37
|
+
|
|
38
|
+
return cmd
|
|
39
|
+
|
|
40
|
+
def ask(
|
|
41
|
+
self,
|
|
42
|
+
message: str,
|
|
43
|
+
session_id: str | None = None,
|
|
44
|
+
model: str | None = None,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""Send a message to Gemini and get a response."""
|
|
47
|
+
cmd = self.build_command(message, session_id, model)
|
|
48
|
+
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
cmd,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if result.returncode != 0:
|
|
56
|
+
return {
|
|
57
|
+
"response": None,
|
|
58
|
+
"session_id": session_id,
|
|
59
|
+
"error": result.stderr or "Command failed",
|
|
60
|
+
"raw": result.stdout,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Try to parse JSON output
|
|
64
|
+
try:
|
|
65
|
+
data = json.loads(result.stdout)
|
|
66
|
+
# Extract response - structure may vary
|
|
67
|
+
response_text = ""
|
|
68
|
+
new_session_id = session_id
|
|
69
|
+
|
|
70
|
+
if isinstance(data, dict):
|
|
71
|
+
response_text = data.get("result", data.get("response", data.get("text", "")))
|
|
72
|
+
new_session_id = data.get("session_id", data.get("sessionId", session_id))
|
|
73
|
+
elif isinstance(data, str):
|
|
74
|
+
response_text = data
|
|
75
|
+
|
|
76
|
+
# For continuation, use "latest" as session identifier
|
|
77
|
+
if not new_session_id:
|
|
78
|
+
new_session_id = "latest"
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
"response": response_text,
|
|
82
|
+
"session_id": new_session_id,
|
|
83
|
+
"raw": data,
|
|
84
|
+
}
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
# Plain text output
|
|
87
|
+
return {
|
|
88
|
+
"response": result.stdout.strip(),
|
|
89
|
+
"session_id": "latest", # Use "latest" for continuation
|
|
90
|
+
"raw": result.stdout,
|
|
91
|
+
}
|