hire-ai 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- hire/adapters/__init__.py +21 -0
- hire/adapters/base.py +42 -0
- hire/adapters/claude.py +74 -0
- hire/adapters/codex.py +98 -0
- hire/adapters/gemini.py +91 -0
- hire/cli.py +164 -0
- hire/commands/__init__.py +8 -0
- hire/commands/ask.py +143 -0
- hire/commands/delete.py +35 -0
- hire/commands/sessions.py +38 -0
- hire/commands/show.py +31 -0
- hire/config.py +48 -0
- hire/paths.py +53 -0
- hire/session.py +163 -0
- hire_ai-0.1.0.dist-info/METADATA +125 -0
- hire_ai-0.1.0.dist-info/RECORD +20 -0
- hire_ai-0.1.0.dist-info/WHEEL +4 -0
- hire_ai-0.1.0.dist-info/entry_points.txt +2 -0
- hire_ai-0.1.0.dist-info/licenses/LICENSE +21 -0
hire/__init__.py
ADDED
|
@@ -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"]
|
hire/adapters/base.py
ADDED
|
@@ -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
|
hire/adapters/claude.py
ADDED
|
@@ -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
|
+
}
|
hire/adapters/codex.py
ADDED
|
@@ -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
|
+
}
|
hire/adapters/gemini.py
ADDED
|
@@ -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
|
+
}
|
hire/cli.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""CLI entry point."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import __version__
|
|
7
|
+
from .commands import run_ask, run_delete, run_sessions, run_show
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
SUBCOMMANDS = {"sessions", "show", "delete", "help", "--help", "-h", "--version"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> int:
|
|
14
|
+
"""Main entry point."""
|
|
15
|
+
# Check if first arg is a subcommand, if not, treat as default (hire) action
|
|
16
|
+
if len(sys.argv) > 1 and sys.argv[1] not in SUBCOMMANDS:
|
|
17
|
+
# Default action: hire an agent
|
|
18
|
+
return run_default()
|
|
19
|
+
|
|
20
|
+
# Subcommand mode
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog="hire",
|
|
23
|
+
description="Hire AI agents to do tasks (Claude, Codex, Gemini)",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"--version",
|
|
27
|
+
action="version",
|
|
28
|
+
version=f"%(prog)s {__version__}",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
32
|
+
|
|
33
|
+
# sessions command
|
|
34
|
+
sessions_parser = subparsers.add_parser("sessions", help="List sessions")
|
|
35
|
+
sessions_parser.add_argument(
|
|
36
|
+
"target",
|
|
37
|
+
nargs="?",
|
|
38
|
+
choices=["claude", "codex", "gemini"],
|
|
39
|
+
help="Filter by agent",
|
|
40
|
+
)
|
|
41
|
+
sessions_parser.add_argument(
|
|
42
|
+
"--json",
|
|
43
|
+
action="store_true",
|
|
44
|
+
help="Output in JSON format",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# show command
|
|
48
|
+
show_parser = subparsers.add_parser("show", help="Show session details")
|
|
49
|
+
show_parser.add_argument(
|
|
50
|
+
"name_or_id",
|
|
51
|
+
help="Session name or ID",
|
|
52
|
+
)
|
|
53
|
+
show_parser.add_argument(
|
|
54
|
+
"--json",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Output in JSON format",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# delete command
|
|
60
|
+
delete_parser = subparsers.add_parser("delete", help="Delete a session")
|
|
61
|
+
delete_parser.add_argument(
|
|
62
|
+
"name_or_id",
|
|
63
|
+
help="Session name or ID",
|
|
64
|
+
)
|
|
65
|
+
delete_parser.add_argument(
|
|
66
|
+
"-f", "--force",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Delete without confirmation",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
args = parser.parse_args()
|
|
72
|
+
|
|
73
|
+
if args.command is None:
|
|
74
|
+
print_usage()
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
# Dispatch to command handlers
|
|
78
|
+
if args.command == "sessions":
|
|
79
|
+
return run_sessions(args)
|
|
80
|
+
elif args.command == "show":
|
|
81
|
+
return run_show(args)
|
|
82
|
+
elif args.command == "delete":
|
|
83
|
+
return run_delete(args)
|
|
84
|
+
else:
|
|
85
|
+
print_usage()
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_default() -> int:
|
|
90
|
+
"""Run the default hire action."""
|
|
91
|
+
parser = argparse.ArgumentParser(
|
|
92
|
+
prog="hire",
|
|
93
|
+
description="Hire AI agents to do tasks",
|
|
94
|
+
usage="hire [options] <target> <message>\n hire <subcommand> [args]",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"target",
|
|
98
|
+
nargs="?",
|
|
99
|
+
help="Target agent: claude, codex, or gemini",
|
|
100
|
+
)
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"message",
|
|
103
|
+
nargs="?",
|
|
104
|
+
help="Message to send",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"-c", "--continue",
|
|
108
|
+
dest="continue_session",
|
|
109
|
+
action="store_true",
|
|
110
|
+
help="Continue the latest session",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"-s", "--session",
|
|
114
|
+
help="Continue a specific session (by name or ID)",
|
|
115
|
+
)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"-n", "--name",
|
|
118
|
+
help="Name for the session",
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"-m", "--model",
|
|
122
|
+
help="Model to use",
|
|
123
|
+
)
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"--json",
|
|
126
|
+
action="store_true",
|
|
127
|
+
help="Output in JSON format",
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
args = parser.parse_args()
|
|
131
|
+
return run_ask(args)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def print_usage():
|
|
135
|
+
"""Print usage information."""
|
|
136
|
+
print("""hire - Hire AI agents to do tasks (Claude, Codex, Gemini)
|
|
137
|
+
|
|
138
|
+
Usage:
|
|
139
|
+
hire <target> <message> Hire an agent to do a task
|
|
140
|
+
hire -s <session> <message> Continue a specific session
|
|
141
|
+
hire sessions [target] List sessions
|
|
142
|
+
hire show <name-or-id> Show session details
|
|
143
|
+
hire delete <name-or-id> Delete a session
|
|
144
|
+
|
|
145
|
+
Targets:
|
|
146
|
+
claude, codex, gemini
|
|
147
|
+
|
|
148
|
+
Options:
|
|
149
|
+
-c, --continue Continue the latest session
|
|
150
|
+
-s, --session ID Continue a specific session
|
|
151
|
+
-n, --name NAME Name the session
|
|
152
|
+
-m, --model MODEL Specify model
|
|
153
|
+
--json Output in JSON format
|
|
154
|
+
|
|
155
|
+
Examples:
|
|
156
|
+
hire codex "Design a REST API"
|
|
157
|
+
hire gemini "Research React 19 features" --json
|
|
158
|
+
hire -s abc123 "Tell me more"
|
|
159
|
+
hire sessions codex
|
|
160
|
+
""")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
sys.exit(main())
|
hire/commands/ask.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Ask command implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from ..adapters import get_adapter
|
|
8
|
+
from ..session import (
|
|
9
|
+
create_session,
|
|
10
|
+
find_session,
|
|
11
|
+
get_latest_session,
|
|
12
|
+
list_sessions,
|
|
13
|
+
save_session,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
VALID_TARGETS = {"claude", "codex", "gemini"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_ask(args: Namespace) -> int:
|
|
21
|
+
"""Run the ask command."""
|
|
22
|
+
target = args.target
|
|
23
|
+
message = args.message
|
|
24
|
+
continue_session = getattr(args, "continue_session", False)
|
|
25
|
+
session_id = args.session
|
|
26
|
+
name = args.name
|
|
27
|
+
model = args.model
|
|
28
|
+
output_json = args.json
|
|
29
|
+
|
|
30
|
+
# Handle case where target is actually the message (when target is omitted)
|
|
31
|
+
# e.g., "delegate 'message'" -> target='message', message=None
|
|
32
|
+
if target and target not in VALID_TARGETS and message is None:
|
|
33
|
+
message = target
|
|
34
|
+
target = None
|
|
35
|
+
|
|
36
|
+
# Load config for defaults
|
|
37
|
+
from ..config import load_config
|
|
38
|
+
config = load_config()
|
|
39
|
+
|
|
40
|
+
if not target:
|
|
41
|
+
target = config.get("defaults", {}).get("agent")
|
|
42
|
+
|
|
43
|
+
# Determine which session to use
|
|
44
|
+
cli_session_id = None
|
|
45
|
+
existing_session = None
|
|
46
|
+
|
|
47
|
+
if session_id:
|
|
48
|
+
# Use specified session
|
|
49
|
+
existing_session = find_session(session_id)
|
|
50
|
+
if existing_session:
|
|
51
|
+
cli_session_id = existing_session.get("cli_session_id")
|
|
52
|
+
# If target not specified, get it from session
|
|
53
|
+
if not target:
|
|
54
|
+
target = existing_session.get("agent")
|
|
55
|
+
else:
|
|
56
|
+
print(f"Error: Session not found: {session_id}", file=sys.stderr)
|
|
57
|
+
return 1
|
|
58
|
+
elif name:
|
|
59
|
+
# Check if named session exists
|
|
60
|
+
existing_session = find_session(name)
|
|
61
|
+
if existing_session:
|
|
62
|
+
if continue_session:
|
|
63
|
+
cli_session_id = existing_session.get("cli_session_id")
|
|
64
|
+
# If target not specified, get it from session
|
|
65
|
+
if not target:
|
|
66
|
+
target = existing_session.get("agent")
|
|
67
|
+
# else: create new session with this name (will replace)
|
|
68
|
+
elif continue_session:
|
|
69
|
+
# Continue latest session
|
|
70
|
+
if not target:
|
|
71
|
+
# Try to find latest session across all agents
|
|
72
|
+
sessions = list_sessions()
|
|
73
|
+
if sessions:
|
|
74
|
+
existing_session = sessions[0]
|
|
75
|
+
target = existing_session.get("agent")
|
|
76
|
+
else:
|
|
77
|
+
existing_session = get_latest_session(target)
|
|
78
|
+
|
|
79
|
+
if existing_session:
|
|
80
|
+
cli_session_id = existing_session.get("cli_session_id")
|
|
81
|
+
else:
|
|
82
|
+
print(f"Warning: No previous session found{' for ' + target if target else ''}, starting new session", file=sys.stderr)
|
|
83
|
+
|
|
84
|
+
# Validate target
|
|
85
|
+
if not target:
|
|
86
|
+
print("Error: Target agent is required (claude, codex, or gemini)", file=sys.stderr)
|
|
87
|
+
return 1
|
|
88
|
+
|
|
89
|
+
# Validate message
|
|
90
|
+
if not message:
|
|
91
|
+
print("Error: Message is required", file=sys.stderr)
|
|
92
|
+
print("Usage: hire <target> <message>", file=sys.stderr)
|
|
93
|
+
return 1
|
|
94
|
+
|
|
95
|
+
# Get the adapter for the target agent
|
|
96
|
+
try:
|
|
97
|
+
adapter = get_adapter(target)
|
|
98
|
+
except ValueError as e:
|
|
99
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
# Call the agent
|
|
103
|
+
result = adapter.ask(message, session_id=cli_session_id, model=model)
|
|
104
|
+
|
|
105
|
+
if result.get("error"):
|
|
106
|
+
print(f"Error: {result['error']}", file=sys.stderr)
|
|
107
|
+
if result.get("raw"):
|
|
108
|
+
print(f"Raw output: {result['raw']}", file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
|
|
111
|
+
# Get the new session ID from the response
|
|
112
|
+
new_cli_session_id = result.get("session_id")
|
|
113
|
+
|
|
114
|
+
# Save or update session
|
|
115
|
+
if existing_session and cli_session_id:
|
|
116
|
+
# Update existing session
|
|
117
|
+
existing_session["cli_session_id"] = new_cli_session_id or cli_session_id
|
|
118
|
+
if name:
|
|
119
|
+
existing_session["name"] = name
|
|
120
|
+
save_session(existing_session)
|
|
121
|
+
session = existing_session
|
|
122
|
+
else:
|
|
123
|
+
# Create new session
|
|
124
|
+
session = create_session(
|
|
125
|
+
agent=target,
|
|
126
|
+
cli_session_id=new_cli_session_id or "unknown",
|
|
127
|
+
name=name,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Output
|
|
131
|
+
if output_json:
|
|
132
|
+
output = {
|
|
133
|
+
"response": result.get("response"),
|
|
134
|
+
"session_id": session["id"],
|
|
135
|
+
"cli_session_id": session["cli_session_id"],
|
|
136
|
+
"agent": target,
|
|
137
|
+
"name": session.get("name"),
|
|
138
|
+
}
|
|
139
|
+
print(json.dumps(output, indent=2, ensure_ascii=False))
|
|
140
|
+
else:
|
|
141
|
+
print(result.get("response", ""))
|
|
142
|
+
|
|
143
|
+
return 0
|
hire/commands/delete.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Delete command implementation."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
|
|
6
|
+
from ..session import delete_session, find_session
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_delete(args: Namespace) -> int:
|
|
10
|
+
"""Run the delete command."""
|
|
11
|
+
name_or_id = args.name_or_id
|
|
12
|
+
force = getattr(args, "force", False)
|
|
13
|
+
|
|
14
|
+
session = find_session(name_or_id)
|
|
15
|
+
|
|
16
|
+
if not session:
|
|
17
|
+
print(f"Error: Session not found: {name_or_id}", file=sys.stderr)
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
# Confirm deletion unless --force
|
|
21
|
+
if not force:
|
|
22
|
+
agent = session.get("agent", "")
|
|
23
|
+
name = session.get("name", "-")
|
|
24
|
+
print(f"Delete session {session['id'][:8]} ({agent}, name={name})?")
|
|
25
|
+
response = input("Type 'yes' to confirm: ")
|
|
26
|
+
if response.lower() != "yes":
|
|
27
|
+
print("Cancelled")
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
if delete_session(session):
|
|
31
|
+
print(f"Deleted session: {session['id'][:8]}")
|
|
32
|
+
return 0
|
|
33
|
+
else:
|
|
34
|
+
print(f"Error: Failed to delete session", file=sys.stderr)
|
|
35
|
+
return 1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Sessions command implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from argparse import Namespace
|
|
5
|
+
|
|
6
|
+
from ..session import list_sessions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_sessions(args: Namespace) -> int:
|
|
10
|
+
"""Run the sessions command."""
|
|
11
|
+
target = args.target
|
|
12
|
+
output_json = getattr(args, "json", False)
|
|
13
|
+
|
|
14
|
+
sessions = list_sessions(agent=target)
|
|
15
|
+
|
|
16
|
+
if output_json:
|
|
17
|
+
print(json.dumps(sessions, indent=2, ensure_ascii=False))
|
|
18
|
+
else:
|
|
19
|
+
if not sessions:
|
|
20
|
+
if target:
|
|
21
|
+
print(f"No sessions found for {target}")
|
|
22
|
+
else:
|
|
23
|
+
print("No sessions found")
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
# Print table header
|
|
27
|
+
print(f"{'AGENT':<10} {'NAME':<20} {'ID':<10} {'UPDATED':<20}")
|
|
28
|
+
print("-" * 62)
|
|
29
|
+
|
|
30
|
+
for session in sessions:
|
|
31
|
+
agent = session.get("agent", "")
|
|
32
|
+
name = session.get("name", "-") or "-"
|
|
33
|
+
session_id = session.get("id", "")[:8]
|
|
34
|
+
updated = session.get("updated_at", "")[:19].replace("T", " ")
|
|
35
|
+
|
|
36
|
+
print(f"{agent:<10} {name:<20} {session_id:<10} {updated:<20}")
|
|
37
|
+
|
|
38
|
+
return 0
|
hire/commands/show.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Show command implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from argparse import Namespace
|
|
6
|
+
|
|
7
|
+
from ..session import find_session
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_show(args: Namespace) -> int:
|
|
11
|
+
"""Run the show command."""
|
|
12
|
+
name_or_id = args.name_or_id
|
|
13
|
+
output_json = getattr(args, "json", False)
|
|
14
|
+
|
|
15
|
+
session = find_session(name_or_id)
|
|
16
|
+
|
|
17
|
+
if not session:
|
|
18
|
+
print(f"Error: Session not found: {name_or_id}", file=sys.stderr)
|
|
19
|
+
return 1
|
|
20
|
+
|
|
21
|
+
if output_json:
|
|
22
|
+
print(json.dumps(session, indent=2, ensure_ascii=False))
|
|
23
|
+
else:
|
|
24
|
+
print(f"Session: {session.get('id')}")
|
|
25
|
+
print(f"Agent: {session.get('agent')}")
|
|
26
|
+
print(f"Name: {session.get('name', '-')}")
|
|
27
|
+
print(f"CLI ID: {session.get('cli_session_id')}")
|
|
28
|
+
print(f"Created: {session.get('created_at')}")
|
|
29
|
+
print(f"Updated: {session.get('updated_at')}")
|
|
30
|
+
|
|
31
|
+
return 0
|
hire/config.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Configuration management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .paths import get_config_path
|
|
7
|
+
|
|
8
|
+
DEFAULT_CONFIG = {
|
|
9
|
+
"adapters": {
|
|
10
|
+
"claude": {
|
|
11
|
+
"command": "claude",
|
|
12
|
+
"args": ["--dangerously-skip-permissions"]
|
|
13
|
+
},
|
|
14
|
+
"codex": {
|
|
15
|
+
"command": "codex",
|
|
16
|
+
"args": ["--full-auto"]
|
|
17
|
+
},
|
|
18
|
+
"gemini": {
|
|
19
|
+
"command": "gemini",
|
|
20
|
+
"args": ["-y"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"defaults": {
|
|
24
|
+
"agent": "claude"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_config() -> dict[str, Any]:
|
|
30
|
+
"""Load configuration from file, or return defaults."""
|
|
31
|
+
config_path = get_config_path()
|
|
32
|
+
if config_path.exists():
|
|
33
|
+
with open(config_path) as f:
|
|
34
|
+
return json.load(f)
|
|
35
|
+
return DEFAULT_CONFIG.copy()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def save_config(config: dict[str, Any]) -> None:
|
|
39
|
+
"""Save configuration to file."""
|
|
40
|
+
config_path = get_config_path()
|
|
41
|
+
with open(config_path, "w") as f:
|
|
42
|
+
json.dump(config, f, indent=2)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_adapter_config(agent: str) -> dict[str, Any]:
|
|
46
|
+
"""Get configuration for a specific adapter."""
|
|
47
|
+
config = load_config()
|
|
48
|
+
return config.get("adapters", {}).get(agent, {})
|
hire/paths.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""XDG-compliant path management for hire CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
APP_NAME = "hire"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_config_dir() -> Path:
|
|
10
|
+
"""Get XDG config directory (~/.config/hire/)."""
|
|
11
|
+
xdg_config = os.environ.get("XDG_CONFIG_HOME", "")
|
|
12
|
+
if xdg_config:
|
|
13
|
+
base = Path(xdg_config)
|
|
14
|
+
else:
|
|
15
|
+
base = Path.home() / ".config"
|
|
16
|
+
|
|
17
|
+
config_dir = base / APP_NAME
|
|
18
|
+
config_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
return config_dir
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_data_dir() -> Path:
|
|
23
|
+
"""Get XDG data directory (~/.local/share/hire/)."""
|
|
24
|
+
xdg_data = os.environ.get("XDG_DATA_HOME", "")
|
|
25
|
+
if xdg_data:
|
|
26
|
+
base = Path(xdg_data)
|
|
27
|
+
else:
|
|
28
|
+
base = Path.home() / ".local" / "share"
|
|
29
|
+
|
|
30
|
+
data_dir = base / APP_NAME
|
|
31
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
return data_dir
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_config_path() -> Path:
|
|
36
|
+
"""Get config file path (~/.config/hire/config.json)."""
|
|
37
|
+
return get_config_dir() / "config.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_sessions_dir(agent: str | None = None) -> Path:
|
|
41
|
+
"""Get sessions directory for an agent.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
agent: Agent name (claude, codex, gemini). If None, returns base sessions dir.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Path to sessions directory.
|
|
48
|
+
"""
|
|
49
|
+
sessions_dir = get_data_dir() / "sessions"
|
|
50
|
+
if agent:
|
|
51
|
+
sessions_dir = sessions_dir / agent
|
|
52
|
+
sessions_dir.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
return sessions_dir
|
hire/session.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Session management."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .paths import get_data_dir, get_sessions_dir
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_session(agent: str, cli_session_id: str, name: str | None = None) -> dict[str, Any]:
|
|
12
|
+
"""Create a new session."""
|
|
13
|
+
session = {
|
|
14
|
+
"id": str(uuid.uuid4()),
|
|
15
|
+
"cli_session_id": cli_session_id,
|
|
16
|
+
"agent": agent,
|
|
17
|
+
"name": name,
|
|
18
|
+
"created_at": datetime.now().isoformat(),
|
|
19
|
+
"updated_at": datetime.now().isoformat(),
|
|
20
|
+
}
|
|
21
|
+
save_session(session)
|
|
22
|
+
return session
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def save_session(session: dict[str, Any]) -> None:
|
|
26
|
+
"""Save a session to file."""
|
|
27
|
+
session["updated_at"] = datetime.now().isoformat()
|
|
28
|
+
sessions_dir = get_sessions_dir(session["agent"])
|
|
29
|
+
|
|
30
|
+
# Use session ID as filename (1 file per session)
|
|
31
|
+
filename = f"{session['id']}.json"
|
|
32
|
+
filepath = sessions_dir / filename
|
|
33
|
+
|
|
34
|
+
with open(filepath, "w") as f:
|
|
35
|
+
json.dump(session, f, indent=2)
|
|
36
|
+
|
|
37
|
+
# Update latest pointer
|
|
38
|
+
latest_path = sessions_dir / "latest.json"
|
|
39
|
+
with open(latest_path, "w") as f:
|
|
40
|
+
json.dump({"session_id": session["id"], "filename": filename}, f)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_latest_session(agent: str) -> dict[str, Any] | None:
|
|
44
|
+
"""Get the latest session for an agent."""
|
|
45
|
+
sessions_dir = get_sessions_dir(agent)
|
|
46
|
+
latest_path = sessions_dir / "latest.json"
|
|
47
|
+
|
|
48
|
+
if not latest_path.exists():
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
with open(latest_path) as f:
|
|
52
|
+
latest = json.load(f)
|
|
53
|
+
|
|
54
|
+
filepath = sessions_dir / latest["filename"]
|
|
55
|
+
if not filepath.exists():
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
with open(filepath) as f:
|
|
59
|
+
return json.load(f)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_session_by_id(session_id: str) -> dict[str, Any] | None:
|
|
63
|
+
"""Get a session by its ID (searches all agents)."""
|
|
64
|
+
sessions_base = get_sessions_dir()
|
|
65
|
+
|
|
66
|
+
if not sessions_base.exists():
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
for agent_dir in sessions_base.iterdir():
|
|
70
|
+
if not agent_dir.is_dir():
|
|
71
|
+
continue
|
|
72
|
+
for session_file in agent_dir.glob("*.json"):
|
|
73
|
+
if session_file.name == "latest.json":
|
|
74
|
+
continue
|
|
75
|
+
with open(session_file) as f:
|
|
76
|
+
session = json.load(f)
|
|
77
|
+
if session["id"] == session_id or session["id"].startswith(session_id):
|
|
78
|
+
return session
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_session_by_name(name: str) -> dict[str, Any] | None:
|
|
83
|
+
"""Get a session by its name (searches all agents)."""
|
|
84
|
+
sessions_base = get_sessions_dir()
|
|
85
|
+
|
|
86
|
+
if not sessions_base.exists():
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
for agent_dir in sessions_base.iterdir():
|
|
90
|
+
if not agent_dir.is_dir():
|
|
91
|
+
continue
|
|
92
|
+
for session_file in agent_dir.glob("*.json"):
|
|
93
|
+
if session_file.name == "latest.json":
|
|
94
|
+
continue
|
|
95
|
+
with open(session_file) as f:
|
|
96
|
+
session = json.load(f)
|
|
97
|
+
if session.get("name") == name:
|
|
98
|
+
return session
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def find_session(name_or_id: str) -> dict[str, Any] | None:
|
|
103
|
+
"""Find a session by name or ID."""
|
|
104
|
+
# Try by name first
|
|
105
|
+
session = get_session_by_name(name_or_id)
|
|
106
|
+
if session:
|
|
107
|
+
return session
|
|
108
|
+
|
|
109
|
+
# Then try by ID
|
|
110
|
+
return get_session_by_id(name_or_id)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def list_sessions(agent: str | None = None) -> list[dict[str, Any]]:
|
|
114
|
+
"""List all sessions, optionally filtered by agent."""
|
|
115
|
+
sessions_base = get_sessions_dir()
|
|
116
|
+
|
|
117
|
+
if not sessions_base.exists():
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
sessions = []
|
|
121
|
+
|
|
122
|
+
if agent:
|
|
123
|
+
agent_dirs = [sessions_base / agent]
|
|
124
|
+
else:
|
|
125
|
+
agent_dirs = [d for d in sessions_base.iterdir() if d.is_dir()]
|
|
126
|
+
|
|
127
|
+
for agent_dir in agent_dirs:
|
|
128
|
+
if not agent_dir.exists():
|
|
129
|
+
continue
|
|
130
|
+
for session_file in agent_dir.glob("*.json"):
|
|
131
|
+
if session_file.name == "latest.json":
|
|
132
|
+
continue
|
|
133
|
+
with open(session_file) as f:
|
|
134
|
+
sessions.append(json.load(f))
|
|
135
|
+
|
|
136
|
+
# Sort by updated_at descending
|
|
137
|
+
sessions.sort(key=lambda s: s.get("updated_at", ""), reverse=True)
|
|
138
|
+
return sessions
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def delete_session(session: dict[str, Any]) -> bool:
|
|
142
|
+
"""Delete a session."""
|
|
143
|
+
sessions_dir = get_sessions_dir(session["agent"])
|
|
144
|
+
|
|
145
|
+
# Find and delete the session file
|
|
146
|
+
for session_file in sessions_dir.glob("*.json"):
|
|
147
|
+
if session_file.name == "latest.json":
|
|
148
|
+
continue
|
|
149
|
+
with open(session_file) as f:
|
|
150
|
+
s = json.load(f)
|
|
151
|
+
if s["id"] == session["id"]:
|
|
152
|
+
session_file.unlink()
|
|
153
|
+
|
|
154
|
+
# Update latest if needed
|
|
155
|
+
latest_path = sessions_dir / "latest.json"
|
|
156
|
+
if latest_path.exists():
|
|
157
|
+
with open(latest_path) as f:
|
|
158
|
+
latest = json.load(f)
|
|
159
|
+
if latest["session_id"] == session["id"]:
|
|
160
|
+
latest_path.unlink()
|
|
161
|
+
|
|
162
|
+
return True
|
|
163
|
+
return False
|
|
@@ -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,20 @@
|
|
|
1
|
+
hire/__init__.py,sha256=-V9lKavG7egEg5jYKkWGmwMTdpZzvGX_Tkl5sM9U6l0,90
|
|
2
|
+
hire/cli.py,sha256=jGCt6rAKTy_Ydk4sorBAzJ6uSfuuJDeAEVgxyZ14pqg,4244
|
|
3
|
+
hire/config.py,sha256=s058dSnojmIdT1KRULWXchdMv6WM9aZ8CKgCqMGdVvA,1164
|
|
4
|
+
hire/paths.py,sha256=PMLwEil1qRmW-9fJ-nTnvWZOUuJvMc-N54xrPp2NHfA,1372
|
|
5
|
+
hire/session.py,sha256=Y-A2zCqiVshEOkBTNTAgaYu_FtCy1JBp4R91NMs5ffY,4874
|
|
6
|
+
hire/adapters/__init__.py,sha256=Hl3ZpDFaWQL0RSKT_XaNTWHwMP6VQMorq7Ce_pn3qpU,612
|
|
7
|
+
hire/adapters/base.py,sha256=rubjoIXu7S8nIhBhksejR_Pb5YG4kwTZUj3dAMff_l4,1105
|
|
8
|
+
hire/adapters/claude.py,sha256=tM7G25jGqinjK_7QjtF_N0E089V9p1dzNReXJnq0VO4,1929
|
|
9
|
+
hire/adapters/codex.py,sha256=_4MXGTz-0YujuutDVCliaj9P94bm-Z1yNEElRtN3wzA,2895
|
|
10
|
+
hire/adapters/gemini.py,sha256=Qm_e1zzhxu2ooXANfFvVCwzxteVatlbDnBZeuf-jHNw,2613
|
|
11
|
+
hire/commands/__init__.py,sha256=sp3kdLPcKmqqgh_PyiOyDxJeHhnCHw0GIaoHWccgSCk,204
|
|
12
|
+
hire/commands/ask.py,sha256=tsM3VK3i06zTQgb0iEAXX97_13b9bHVyX8Ju7dV369g,4490
|
|
13
|
+
hire/commands/delete.py,sha256=1fLZ64xmyIzR5s7i7lsNe-WornsD_qsOAe1cCdOodDU,991
|
|
14
|
+
hire/commands/sessions.py,sha256=aIsh8oC4N3KtMeUdIrCvU3hONMxAfV_Z4qnprBiP62k,1073
|
|
15
|
+
hire/commands/show.py,sha256=gztFedJ553fvru0dPbHyperOMXDew-Yri3E3APu9d3c,860
|
|
16
|
+
hire_ai-0.1.0.dist-info/METADATA,sha256=FCFDGQIV4XBQ7ZhLCHq9hlJ6HRP2nahzJw3ievV3y9s,3151
|
|
17
|
+
hire_ai-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
hire_ai-0.1.0.dist-info/entry_points.txt,sha256=oq7wq8Ju9C2Lw79ljxSkQIaeKOHA_aIN9JKQAaDbz_Q,39
|
|
19
|
+
hire_ai-0.1.0.dist-info/licenses/LICENSE,sha256=Hj8NYmaOGk4xCcCYqD_5U91CVJwcJXEFSMWS965dUI0,1064
|
|
20
|
+
hire_ai-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|