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.
@@ -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
@@ -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,3 @@
1
+ """hire - CLI to orchestrate AI agents (Claude, Codex, Gemini)."""
2
+
3
+ __version__ = "0.1.0"
@@ -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
+ }