orchex-cli 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.
- orchex/__init__.py +1 -0
- orchex/__main__.py +2 -0
- orchex/adapters/__init__.py +0 -0
- orchex/adapters/base.py +40 -0
- orchex/adapters/claude.py +62 -0
- orchex/adapters/codex.py +46 -0
- orchex/adapters/gemini.py +46 -0
- orchex/adapters/registry.py +11 -0
- orchex/cli.py +286 -0
- orchex/config/__init__.py +0 -0
- orchex/config/loader.py +58 -0
- orchex/config/schema.py +23 -0
- orchex/core/__init__.py +0 -0
- orchex/core/consensus.py +138 -0
- orchex/core/engine.py +206 -0
- orchex/core/features.py +33 -0
- orchex/core/instructions.py +38 -0
- orchex/core/pty_runner.py +74 -0
- orchex/core/router.py +55 -0
- orchex/core/task.py +64 -0
- orchex/core/worktree.py +86 -0
- orchex/daemon/__init__.py +0 -0
- orchex/daemon/ipc.py +66 -0
- orchex/daemon/protocol.py +37 -0
- orchex/daemon/server.py +241 -0
- orchex/notify/__init__.py +0 -0
- orchex/notify/base.py +39 -0
- orchex/notify/discord.py +25 -0
- orchex/notify/telegram.py +27 -0
- orchex_cli-0.1.0.dist-info/METADATA +110 -0
- orchex_cli-0.1.0.dist-info/RECORD +33 -0
- orchex_cli-0.1.0.dist-info/WHEEL +4 -0
- orchex_cli-0.1.0.dist-info/entry_points.txt +2 -0
orchex/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
orchex/__main__.py
ADDED
|
File without changes
|
orchex/adapters/base.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AdapterFeature:
|
|
9
|
+
name: str
|
|
10
|
+
description: str
|
|
11
|
+
enabled: bool = True
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AgentAdapter(ABC):
|
|
15
|
+
name: str = ""
|
|
16
|
+
config_dir: Path = Path()
|
|
17
|
+
instruction_files: Dict[str, str] = {}
|
|
18
|
+
|
|
19
|
+
@abstractmethod
|
|
20
|
+
async def detect(self):
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def send_prompt(self, prompt, working_dir):
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
async def get_version(self):
|
|
29
|
+
...
|
|
30
|
+
|
|
31
|
+
async def list_features(self):
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
async def get_instructions(self, project_dir):
|
|
35
|
+
project_file = self.instruction_files.get("project")
|
|
36
|
+
if project_file:
|
|
37
|
+
path = project_dir / project_file
|
|
38
|
+
if path.exists():
|
|
39
|
+
return path.read_text()
|
|
40
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
from orchex.adapters.base import AdapterFeature, AgentAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClaudeAdapter(AgentAdapter):
|
|
11
|
+
name = "claude"
|
|
12
|
+
config_dir = Path.home() / ".claude"
|
|
13
|
+
instruction_files = {
|
|
14
|
+
"project": "CLAUDE.md",
|
|
15
|
+
"global": str(Path.home() / ".claude" / "CLAUDE.md"),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async def detect(self):
|
|
19
|
+
return shutil.which("claude") is not None
|
|
20
|
+
|
|
21
|
+
async def get_version(self):
|
|
22
|
+
if not await self.detect():
|
|
23
|
+
return None
|
|
24
|
+
proc = await asyncio.create_subprocess_exec(
|
|
25
|
+
"claude", "--version",
|
|
26
|
+
stdout=asyncio.subprocess.PIPE,
|
|
27
|
+
stderr=asyncio.subprocess.PIPE,
|
|
28
|
+
)
|
|
29
|
+
stdout, _ = await proc.communicate()
|
|
30
|
+
return stdout.decode().strip() if proc.returncode == 0 else None
|
|
31
|
+
|
|
32
|
+
async def send_prompt(self, prompt, working_dir):
|
|
33
|
+
proc = await asyncio.create_subprocess_exec(
|
|
34
|
+
"claude", "-p", prompt, "--output-format", "json",
|
|
35
|
+
cwd=str(working_dir),
|
|
36
|
+
stdout=asyncio.subprocess.PIPE,
|
|
37
|
+
stderr=asyncio.subprocess.PIPE,
|
|
38
|
+
)
|
|
39
|
+
stdout, stderr = await proc.communicate()
|
|
40
|
+
|
|
41
|
+
if proc.returncode != 0:
|
|
42
|
+
raise RuntimeError("Claude CLI failed: %s" % stderr.decode())
|
|
43
|
+
|
|
44
|
+
data = json.loads(stdout.decode())
|
|
45
|
+
return data.get("result", "")
|
|
46
|
+
|
|
47
|
+
async def list_features(self):
|
|
48
|
+
features = []
|
|
49
|
+
settings_path = self.config_dir / "settings.json"
|
|
50
|
+
if settings_path.exists():
|
|
51
|
+
try:
|
|
52
|
+
settings = json.loads(settings_path.read_text())
|
|
53
|
+
mcp_servers = settings.get("mcpServers", {})
|
|
54
|
+
for name in mcp_servers:
|
|
55
|
+
features.append(AdapterFeature(
|
|
56
|
+
name="mcp:%s" % name,
|
|
57
|
+
description="MCP server: %s" % name,
|
|
58
|
+
enabled=True,
|
|
59
|
+
))
|
|
60
|
+
except (json.JSONDecodeError, KeyError):
|
|
61
|
+
pass
|
|
62
|
+
return features
|
orchex/adapters/codex.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from orchex.adapters.base import AdapterFeature, AgentAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CodexAdapter(AgentAdapter):
|
|
9
|
+
name = "codex"
|
|
10
|
+
config_dir = Path.home() / ".codex"
|
|
11
|
+
instruction_files = {
|
|
12
|
+
"project": "AGENTS.md",
|
|
13
|
+
"global": str(Path.home() / ".codex" / "instructions.md"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async def detect(self):
|
|
17
|
+
return shutil.which("codex") is not None
|
|
18
|
+
|
|
19
|
+
async def get_version(self):
|
|
20
|
+
if not await self.detect():
|
|
21
|
+
return None
|
|
22
|
+
proc = await asyncio.create_subprocess_exec(
|
|
23
|
+
"codex", "--version",
|
|
24
|
+
stdout=asyncio.subprocess.PIPE,
|
|
25
|
+
stderr=asyncio.subprocess.PIPE,
|
|
26
|
+
)
|
|
27
|
+
stdout, _ = await proc.communicate()
|
|
28
|
+
return stdout.decode().strip() if proc.returncode == 0 else None
|
|
29
|
+
|
|
30
|
+
async def send_prompt(self, prompt, working_dir):
|
|
31
|
+
proc = await asyncio.create_subprocess_exec(
|
|
32
|
+
"codex", "exec", prompt,
|
|
33
|
+
cwd=str(working_dir),
|
|
34
|
+
stdout=asyncio.subprocess.PIPE,
|
|
35
|
+
stderr=asyncio.subprocess.PIPE,
|
|
36
|
+
)
|
|
37
|
+
stdout, stderr = await proc.communicate()
|
|
38
|
+
if proc.returncode != 0:
|
|
39
|
+
raise RuntimeError("Codex CLI failed: %s" % stderr.decode())
|
|
40
|
+
return stdout.decode().strip()
|
|
41
|
+
|
|
42
|
+
async def list_features(self):
|
|
43
|
+
return [
|
|
44
|
+
AdapterFeature(name="sandbox", description="Sandboxed execution mode", enabled=True),
|
|
45
|
+
AdapterFeature(name="auto-edit", description="Automatic file editing", enabled=True),
|
|
46
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from orchex.adapters.base import AdapterFeature, AgentAdapter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GeminiAdapter(AgentAdapter):
|
|
9
|
+
name = "gemini"
|
|
10
|
+
config_dir = Path.home() / ".gemini"
|
|
11
|
+
instruction_files = {
|
|
12
|
+
"project": "GEMINI.md",
|
|
13
|
+
"global": str(Path.home() / ".gemini" / "GEMINI.md"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async def detect(self):
|
|
17
|
+
return shutil.which("gemini") is not None
|
|
18
|
+
|
|
19
|
+
async def get_version(self):
|
|
20
|
+
if not await self.detect():
|
|
21
|
+
return None
|
|
22
|
+
proc = await asyncio.create_subprocess_exec(
|
|
23
|
+
"gemini", "--version",
|
|
24
|
+
stdout=asyncio.subprocess.PIPE,
|
|
25
|
+
stderr=asyncio.subprocess.PIPE,
|
|
26
|
+
)
|
|
27
|
+
stdout, _ = await proc.communicate()
|
|
28
|
+
return stdout.decode().strip() if proc.returncode == 0 else None
|
|
29
|
+
|
|
30
|
+
async def send_prompt(self, prompt, working_dir):
|
|
31
|
+
proc = await asyncio.create_subprocess_exec(
|
|
32
|
+
"gemini", "-p", prompt,
|
|
33
|
+
cwd=str(working_dir),
|
|
34
|
+
stdout=asyncio.subprocess.PIPE,
|
|
35
|
+
stderr=asyncio.subprocess.PIPE,
|
|
36
|
+
)
|
|
37
|
+
stdout, stderr = await proc.communicate()
|
|
38
|
+
if proc.returncode != 0:
|
|
39
|
+
raise RuntimeError("Gemini CLI failed: %s" % stderr.decode())
|
|
40
|
+
return stdout.decode().strip()
|
|
41
|
+
|
|
42
|
+
async def list_features(self):
|
|
43
|
+
return [
|
|
44
|
+
AdapterFeature(name="google-search", description="Google Search integration", enabled=True),
|
|
45
|
+
AdapterFeature(name="multimodal", description="Multimodal input support", enabled=True),
|
|
46
|
+
]
|
orchex/cli.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.rule import Rule
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
|
|
12
|
+
from orchex.core.engine import Engine
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
LOGO = """[bold cyan] ___ ____ ____ _ _ _______ __
|
|
17
|
+
/ _ \\| _ \\ / ___| | | | ____\\ \\/ /
|
|
18
|
+
| | | | |_) | | | |_| | _| \\ /
|
|
19
|
+
| |_| | _ <| |___| _ | |___ / \\
|
|
20
|
+
\\___/|_| \\_\\\\____|_| |_|_____/_/\\_\\[/bold cyan]"""
|
|
21
|
+
|
|
22
|
+
COLORS = {"claude": "cyan", "codex": "green", "gemini": "yellow"}
|
|
23
|
+
|
|
24
|
+
_verbose_mode = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _run(coro):
|
|
28
|
+
return asyncio.run(coro)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def _get_engine():
|
|
32
|
+
engine = Engine()
|
|
33
|
+
await engine.init()
|
|
34
|
+
return engine
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _agent_color(name):
|
|
38
|
+
return COLORS.get(name, "white")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _print_header(engine):
|
|
42
|
+
agents_str = " ".join(
|
|
43
|
+
"[%s]%s[/%s]" % (_agent_color(a), a, _agent_color(a))
|
|
44
|
+
for a in engine.available_agents
|
|
45
|
+
)
|
|
46
|
+
offline = [a.name for a in engine.adapters if a.name not in engine.available_agents]
|
|
47
|
+
if offline:
|
|
48
|
+
agents_str += " " + " ".join("[dim red]%s[/dim red]" % a for a in offline)
|
|
49
|
+
|
|
50
|
+
console.print(LOGO)
|
|
51
|
+
console.print(" [bold]orchex[/bold] v0.1.0")
|
|
52
|
+
console.print(" Multi-Provider Agent Orchestrator")
|
|
53
|
+
console.print(" [dim]%s[/dim]" % Path.cwd())
|
|
54
|
+
console.print()
|
|
55
|
+
console.print(" Agents: %s" % agents_str)
|
|
56
|
+
console.print()
|
|
57
|
+
console.print(Rule(style="dim"))
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.group(invoke_without_command=True)
|
|
62
|
+
@click.pass_context
|
|
63
|
+
def main(ctx):
|
|
64
|
+
"""orchex -- Multi-provider AI coding agent orchestrator.
|
|
65
|
+
|
|
66
|
+
All agents collaborate on every request.
|
|
67
|
+
"""
|
|
68
|
+
if ctx.invoked_subcommand is None:
|
|
69
|
+
_run(_interactive())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def _interactive():
|
|
73
|
+
engine = await _get_engine()
|
|
74
|
+
|
|
75
|
+
if not engine.available_agents:
|
|
76
|
+
console.print("[red]No AI agents found. Install claude, codex, or gemini CLI.[/red]")
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
_print_header(engine)
|
|
80
|
+
|
|
81
|
+
while True:
|
|
82
|
+
try:
|
|
83
|
+
user_input = console.input("[bold]> [/bold]")
|
|
84
|
+
except (EOFError, KeyboardInterrupt):
|
|
85
|
+
console.print("\n[dim]Bye.[/dim]")
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
user_input = user_input.strip()
|
|
89
|
+
if not user_input:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
if user_input.startswith("/"):
|
|
93
|
+
handled = await _handle_command(engine, user_input)
|
|
94
|
+
if handled == "quit":
|
|
95
|
+
break
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
await _orchestrate(engine, user_input)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def _handle_command(engine, cmd):
|
|
102
|
+
parts = cmd.split(None, 1)
|
|
103
|
+
command = parts[0].lower()
|
|
104
|
+
arg = parts[1] if len(parts) > 1 else ""
|
|
105
|
+
|
|
106
|
+
if command in ("/quit", "/exit", "/q"):
|
|
107
|
+
console.print("[dim]Bye.[/dim]")
|
|
108
|
+
return "quit"
|
|
109
|
+
|
|
110
|
+
elif command == "/agents":
|
|
111
|
+
statuses = await engine.get_agent_status()
|
|
112
|
+
table = Table(show_header=True, expand=True, border_style="dim")
|
|
113
|
+
table.add_column("Agent", style="bold")
|
|
114
|
+
table.add_column("Status")
|
|
115
|
+
table.add_column("Version")
|
|
116
|
+
table.add_column("Features", style="dim")
|
|
117
|
+
for s in statuses:
|
|
118
|
+
status = Text("ONLINE", style="green") if s["detected"] else Text("OFFLINE", style="red")
|
|
119
|
+
ver = s.get("version") or "-"
|
|
120
|
+
feats = ", ".join(f["name"] for f in s.get("features", [])[:3]) or "-"
|
|
121
|
+
table.add_row(s["name"], status, ver, feats)
|
|
122
|
+
console.print(table)
|
|
123
|
+
console.print()
|
|
124
|
+
|
|
125
|
+
elif command == "/verbose":
|
|
126
|
+
global _verbose_mode
|
|
127
|
+
_verbose_mode = not _verbose_mode
|
|
128
|
+
state = "ON" if _verbose_mode else "OFF"
|
|
129
|
+
console.print("[dim]Verbose mode: %s[/dim]" % state)
|
|
130
|
+
|
|
131
|
+
elif command == "/help":
|
|
132
|
+
console.print(Panel(
|
|
133
|
+
"[bold]Commands:[/bold]\n"
|
|
134
|
+
" /agents Show agent status\n"
|
|
135
|
+
" /verbose Toggle detailed output (responses + reviews)\n"
|
|
136
|
+
" /help This help\n"
|
|
137
|
+
" /quit Exit\n"
|
|
138
|
+
"\n"
|
|
139
|
+
"[bold]How it works:[/bold]\n"
|
|
140
|
+
" Type any question or task. All agents work on it\n"
|
|
141
|
+
" independently, then cross-review each other's work,\n"
|
|
142
|
+
" and deliver a consolidated answer.",
|
|
143
|
+
title="Help",
|
|
144
|
+
border_style="dim",
|
|
145
|
+
))
|
|
146
|
+
|
|
147
|
+
else:
|
|
148
|
+
console.print("[yellow]Unknown command. Type /help[/yellow]")
|
|
149
|
+
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def _orchestrate(engine, user_input):
|
|
154
|
+
"""Full pipeline with live status indicators."""
|
|
155
|
+
import time
|
|
156
|
+
|
|
157
|
+
agent_status = {name: "waiting" for name in engine.available_agents}
|
|
158
|
+
current_phase = ""
|
|
159
|
+
start_time = time.time()
|
|
160
|
+
|
|
161
|
+
def _status_line():
|
|
162
|
+
"""Build a dynamic status line showing each agent's state."""
|
|
163
|
+
parts = []
|
|
164
|
+
for name in engine.available_agents:
|
|
165
|
+
c = _agent_color(name)
|
|
166
|
+
st = agent_status[name]
|
|
167
|
+
if st == "waiting":
|
|
168
|
+
parts.append("[dim]%s[/dim]" % name)
|
|
169
|
+
elif st == "working":
|
|
170
|
+
parts.append("[bold %s]%s ...[/bold %s]" % (c, name, c))
|
|
171
|
+
elif st == "done":
|
|
172
|
+
parts.append("[%s]%s OK[/%s]" % (c, name, c))
|
|
173
|
+
elif st == "error":
|
|
174
|
+
parts.append("[red]%s ERR[/red]" % name)
|
|
175
|
+
elapsed = "%.0fs" % (time.time() - start_time)
|
|
176
|
+
return "[dim][%s][/dim] %s [dim]%s[/dim]" % (current_phase, " ".join(parts), elapsed)
|
|
177
|
+
|
|
178
|
+
def on_agent_done(agent_name, result):
|
|
179
|
+
if result.get("ok"):
|
|
180
|
+
agent_status[agent_name] = "done"
|
|
181
|
+
else:
|
|
182
|
+
agent_status[agent_name] = "error"
|
|
183
|
+
# Update live display
|
|
184
|
+
if live:
|
|
185
|
+
live.update(_status_line())
|
|
186
|
+
|
|
187
|
+
def on_phase(phase, data):
|
|
188
|
+
nonlocal current_phase
|
|
189
|
+
if phase == "asking":
|
|
190
|
+
current_phase = "Phase 1/3: Researching"
|
|
191
|
+
for name in engine.available_agents:
|
|
192
|
+
agent_status[name] = "working"
|
|
193
|
+
elif phase == "responses_done":
|
|
194
|
+
pass
|
|
195
|
+
elif phase == "reviewing":
|
|
196
|
+
current_phase = "Phase 2/3: Cross-Review"
|
|
197
|
+
for name in engine.available_agents:
|
|
198
|
+
if agent_status[name] == "done":
|
|
199
|
+
agent_status[name] = "working"
|
|
200
|
+
elif phase == "reviews_done":
|
|
201
|
+
pass
|
|
202
|
+
elif phase == "synthesizing":
|
|
203
|
+
current_phase = "Phase 3/3: Synthesizing"
|
|
204
|
+
for name in engine.available_agents:
|
|
205
|
+
agent_status[name] = "waiting"
|
|
206
|
+
agent_status[engine.available_agents[0]] = "working"
|
|
207
|
+
if live:
|
|
208
|
+
live.update(_status_line())
|
|
209
|
+
|
|
210
|
+
live = Live(_status_line(), console=console, refresh_per_second=4)
|
|
211
|
+
with live:
|
|
212
|
+
result = await engine.orchestrate(
|
|
213
|
+
user_input, on_phase=on_phase, on_agent_done=on_agent_done
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
elapsed_total = "%.0fs" % (time.time() - start_time)
|
|
217
|
+
console.print()
|
|
218
|
+
|
|
219
|
+
# Verbose: show agent responses and reviews
|
|
220
|
+
if _verbose_mode:
|
|
221
|
+
console.print(Rule("Agent Responses", style="dim"))
|
|
222
|
+
console.print()
|
|
223
|
+
for r in result["responses"]:
|
|
224
|
+
c = _agent_color(r["agent"])
|
|
225
|
+
if r["ok"]:
|
|
226
|
+
console.print("[bold %s]%s[/bold %s]" % (c, r["agent"], c))
|
|
227
|
+
console.print(r["response"][:500])
|
|
228
|
+
console.print()
|
|
229
|
+
|
|
230
|
+
if result["reviews"]:
|
|
231
|
+
console.print(Rule("Cross Reviews", style="dim"))
|
|
232
|
+
console.print()
|
|
233
|
+
for r in result["reviews"]:
|
|
234
|
+
if isinstance(r, dict) and r.get("ok"):
|
|
235
|
+
c = _agent_color(r["agent"])
|
|
236
|
+
console.print("[bold %s]%s review[/bold %s]" % (c, r["agent"], c))
|
|
237
|
+
console.print(r["response"][:500])
|
|
238
|
+
console.print()
|
|
239
|
+
|
|
240
|
+
# Always show final answer
|
|
241
|
+
agent_count = len([r for r in result["responses"] if r["ok"]])
|
|
242
|
+
review_count = len([r for r in result.get("reviews", []) if isinstance(r, dict) and r.get("ok")])
|
|
243
|
+
meta = "[dim]%d agents | %d reviews | %s[/dim]" % (agent_count, review_count, elapsed_total)
|
|
244
|
+
|
|
245
|
+
console.print(Panel(
|
|
246
|
+
result["summary"][:3000],
|
|
247
|
+
title="[bold green]orchex[/bold green]",
|
|
248
|
+
subtitle=meta,
|
|
249
|
+
border_style="green",
|
|
250
|
+
))
|
|
251
|
+
console.print()
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# --- Non-interactive subcommands ---
|
|
255
|
+
|
|
256
|
+
@main.command()
|
|
257
|
+
def agents():
|
|
258
|
+
"""Show agent status."""
|
|
259
|
+
async def _exec():
|
|
260
|
+
engine = await _get_engine()
|
|
261
|
+
statuses = await engine.get_agent_status()
|
|
262
|
+
table = Table(show_header=True, expand=True, border_style="dim")
|
|
263
|
+
table.add_column("Agent", style="bold")
|
|
264
|
+
table.add_column("Status")
|
|
265
|
+
table.add_column("Version")
|
|
266
|
+
table.add_column("Features", style="dim")
|
|
267
|
+
for s in statuses:
|
|
268
|
+
status = Text("ONLINE", style="green") if s["detected"] else Text("OFFLINE", style="red")
|
|
269
|
+
ver = s.get("version") or "-"
|
|
270
|
+
feats = ", ".join(f["name"] for f in s.get("features", [])[:3]) or "-"
|
|
271
|
+
table.add_row(s["name"], status, ver, feats)
|
|
272
|
+
console.print(table)
|
|
273
|
+
_run(_exec())
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@main.command()
|
|
277
|
+
@click.argument("prompt")
|
|
278
|
+
def ask(prompt):
|
|
279
|
+
"""Ask all agents a question (non-interactive)."""
|
|
280
|
+
async def _exec():
|
|
281
|
+
engine = await _get_engine()
|
|
282
|
+
if not engine.available_agents:
|
|
283
|
+
console.print("[red]No agents found.[/red]")
|
|
284
|
+
return
|
|
285
|
+
await _orchestrate(engine, prompt)
|
|
286
|
+
_run(_exec())
|
|
File without changes
|
orchex/config/loader.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
if sys.version_info >= (3, 11):
|
|
6
|
+
import tomllib
|
|
7
|
+
else:
|
|
8
|
+
import tomli as tomllib
|
|
9
|
+
|
|
10
|
+
from orchex.config.schema import OrchexConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _resolve_env(value):
|
|
14
|
+
if isinstance(value, str) and value.startswith("env:"):
|
|
15
|
+
return os.environ.get(value[4:], "")
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_config(project_dir):
|
|
20
|
+
config_path = project_dir / ".orchex" / "config.toml"
|
|
21
|
+
cfg = OrchexConfig()
|
|
22
|
+
|
|
23
|
+
if not config_path.exists():
|
|
24
|
+
return cfg
|
|
25
|
+
|
|
26
|
+
with open(config_path, "rb") as f:
|
|
27
|
+
data = tomllib.load(f)
|
|
28
|
+
|
|
29
|
+
ideation = data.get("ideation", {})
|
|
30
|
+
if "max_rounds" in ideation:
|
|
31
|
+
cfg.max_rounds = ideation["max_rounds"]
|
|
32
|
+
if "token_budget" in ideation:
|
|
33
|
+
cfg.token_budget = ideation["token_budget"]
|
|
34
|
+
if "discussion_mode" in ideation:
|
|
35
|
+
cfg.discussion_mode = ideation["discussion_mode"]
|
|
36
|
+
|
|
37
|
+
routing = data.get("routing", {}).get("defaults", {})
|
|
38
|
+
if routing:
|
|
39
|
+
cfg.routing_defaults.update(routing)
|
|
40
|
+
|
|
41
|
+
daemon = data.get("daemon", {})
|
|
42
|
+
if "socket_path" in daemon:
|
|
43
|
+
cfg.socket_path = daemon["socket_path"]
|
|
44
|
+
if "pid_file" in daemon:
|
|
45
|
+
cfg.pid_file = daemon["pid_file"]
|
|
46
|
+
|
|
47
|
+
tg = data.get("notify", {}).get("telegram", {})
|
|
48
|
+
if tg.get("enabled"):
|
|
49
|
+
cfg.telegram_enabled = True
|
|
50
|
+
cfg.telegram_bot_token = _resolve_env(tg.get("bot_token", ""))
|
|
51
|
+
cfg.telegram_chat_id = _resolve_env(tg.get("chat_id", ""))
|
|
52
|
+
|
|
53
|
+
dc = data.get("notify", {}).get("discord", {})
|
|
54
|
+
if dc.get("enabled"):
|
|
55
|
+
cfg.discord_enabled = True
|
|
56
|
+
cfg.discord_webhook_url = _resolve_env(dc.get("webhook_url", ""))
|
|
57
|
+
|
|
58
|
+
return cfg
|
orchex/config/schema.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class OrchexConfig:
|
|
7
|
+
max_rounds: int = 5
|
|
8
|
+
token_budget: int = 50000
|
|
9
|
+
discussion_mode: str = "free"
|
|
10
|
+
routing_defaults: Dict[str, str] = field(default_factory=lambda: {
|
|
11
|
+
"architecture": "claude",
|
|
12
|
+
"implementation": "codex",
|
|
13
|
+
"research": "gemini",
|
|
14
|
+
"testing": "claude",
|
|
15
|
+
"review": "round-robin",
|
|
16
|
+
})
|
|
17
|
+
socket_path: str = "/tmp/orchex.sock"
|
|
18
|
+
pid_file: str = "/tmp/orchex.pid"
|
|
19
|
+
telegram_enabled: bool = False
|
|
20
|
+
telegram_bot_token: str = ""
|
|
21
|
+
telegram_chat_id: str = ""
|
|
22
|
+
discord_enabled: bool = False
|
|
23
|
+
discord_webhook_url: str = ""
|
orchex/core/__init__.py
ADDED
|
File without changes
|