Skip to content

Commit 49ae1bb

Browse files
Release v0.6.0 (#9)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 0b0849c commit 49ae1bb

10 files changed

Lines changed: 1240 additions & 191 deletions

File tree

dispatch_cli/commands/agent.py

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2515,6 +2515,12 @@ def deploy(
25152515
else:
25162516
logger.error("Authentication failed after retries")
25172517
raise typer.Exit(1)
2518+
elif e.response.status_code == 409:
2519+
logger.error(
2520+
f"A deployment is already in progress for agent '{agent_name}'. "
2521+
"Wait for it to finish or cancel it before deploying again."
2522+
)
2523+
raise typer.Exit(1)
25182524
else:
25192525
logger.error(f"Failed to push image to production: {e}")
25202526
raise typer.Exit(1)
@@ -2594,6 +2600,9 @@ def deploy(
25942600
error = data.get("error", "Unknown error")
25952601
logger.error(f"Deployment failed: {error}")
25962602
raise typer.Exit(1)
2603+
elif job_status == "cancelled":
2604+
logger.warning("Deployment was cancelled.")
2605+
raise typer.Exit(1)
25972606
time.sleep(2)
25982607
except typer.Exit:
25992608
raise
@@ -3091,9 +3100,47 @@ def validate(
30913100
else:
30923101
logger.debug(f"Using existing image: {image_tag}")
30933102

3094-
# 5. Check handler schemas and typed payloads
3103+
# 5. Check dependencies resolve for linux (deployment target)
3104+
logger.info("")
3105+
logger.info("5. Checking dependencies resolve for linux...")
3106+
try:
3107+
dep_check = subprocess.run(
3108+
[
3109+
"uv",
3110+
"sync",
3111+
"--dry-run",
3112+
"--python-platform",
3113+
"linux",
3114+
"--no-install-project",
3115+
],
3116+
capture_output=True,
3117+
text=True,
3118+
cwd=abs_path,
3119+
)
3120+
if dep_check.returncode != 0:
3121+
stderr = dep_check.stderr.strip()
3122+
logger.error("Some dependencies don't have linux wheels:")
3123+
for line in stderr.splitlines():
3124+
if "error:" in line or "hint:" in line:
3125+
logger.error(f" {line.strip()}")
3126+
logger.info(
3127+
"Add tool.uv.required-environments to your pyproject.toml "
3128+
"to ensure your dependencies have linux wheels:"
3129+
)
3130+
logger.info(
3131+
" [tool.uv]\n"
3132+
" required-environments = [\"sys_platform == 'linux'"
3133+
" and platform_machine == 'x86_64'\"]"
3134+
)
3135+
validation_passed = False
3136+
else:
3137+
logger.success("All dependencies have linux-compatible packages.")
3138+
except FileNotFoundError:
3139+
logger.debug("uv not found, skipping linux dependency check.")
3140+
3141+
# 6. Check handler schemas and typed payloads
30953142
logger.info("")
3096-
logger.info("5. Checking handler schemas and typed payloads...")
3143+
logger.info("6. Checking handler schemas and typed payloads...")
30973144
try:
30983145
agent_schemas = extract_handler_schemas_from_agent(abs_path)
30993146
if not agent_schemas:
@@ -3112,9 +3159,9 @@ def validate(
31123159
logger.error(f"Handler schema validation failed: {e}")
31133160
validation_passed = False
31143161

3115-
# 6. Check schema compatibility
3162+
# 7. Check schema compatibility
31163163
logger.info("")
3117-
logger.info("6. Checking schema compatibility...")
3164+
logger.info("7. Checking schema compatibility...")
31183165
try:
31193166
if not agent_schemas:
31203167
agent_schemas = extract_handler_schemas_from_agent(abs_path)
@@ -3130,9 +3177,9 @@ def validate(
31303177
logger.error(f"Schema validation failed: {e}")
31313178
validation_passed = False
31323179

3133-
# 6. Check GitHub integration if agent uses GitHub topics
3180+
# 8. Check GitHub integration if agent uses GitHub topics
31343181
logger.info("")
3135-
logger.info("6. Checking GitHub integration requirements...")
3182+
logger.info("8. Checking GitHub integration requirements...")
31363183
try:
31373184
if agent_schemas:
31383185
github_warnings = check_github_integration_if_needed(

dispatch_cli/commands/mcp.py

Lines changed: 131 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from pathlib import Path
77
from typing import Annotated
88

9+
import tomlkit
910
import typer
1011

1112
from dispatch_cli.auth import get_api_key, get_api_key_from_keychain
@@ -26,6 +27,7 @@ class RegisterMode(StrEnum):
2627
AUTO = "auto"
2728
CLAUDE = "claude"
2829
CURSOR = "cursor"
30+
CODEX = "codex"
2931

3032

3133
def find_git_root() -> Path | None:
@@ -68,6 +70,18 @@ def get_cursor_config_paths() -> list[Path]:
6870
return [project_config] if cursor_dir.exists() else []
6971

7072

73+
def get_codex_config_paths() -> list[Path]:
74+
"""Get Codex MCP config file paths (project-level only)."""
75+
git_root = find_git_root()
76+
if git_root:
77+
codex_dir = git_root / ".codex"
78+
else:
79+
codex_dir = Path(".codex")
80+
81+
config_path = codex_dir / "config.toml"
82+
return [config_path] if codex_dir.exists() else []
83+
84+
7185
def find_mcp_config_files() -> list[tuple[str, Path]]:
7286
"""Find all existing MCP config files.
7387
@@ -85,9 +99,67 @@ def find_mcp_config_files() -> list[tuple[str, Path]]:
8599
if cursor_path.exists():
86100
configs.append(("cursor", cursor_path))
87101

102+
# Check Codex configs (project-level only)
103+
for codex_path in get_codex_config_paths():
104+
if codex_path.exists():
105+
configs.append(("codex", codex_path))
106+
88107
return configs
89108

90109

110+
def write_json_mcp_config(
111+
config_path: Path, server_name: str, server_config: dict
112+
) -> None:
113+
"""Write an MCP server entry to a JSON config file (Claude, Cursor)."""
114+
if config_path.exists():
115+
with open(config_path) as f:
116+
config_data = json.load(f)
117+
else:
118+
config_data = {}
119+
120+
if "mcpServers" not in config_data:
121+
config_data["mcpServers"] = {}
122+
123+
config_data["mcpServers"][server_name] = server_config
124+
125+
config_path.parent.mkdir(parents=True, exist_ok=True)
126+
with open(config_path, "w") as f:
127+
json.dump(config_data, f, indent=2)
128+
f.write("\n")
129+
130+
131+
def write_toml_mcp_config(
132+
config_path: Path, server_name: str, server_config: dict
133+
) -> None:
134+
"""Write an MCP server entry to a TOML config file (Codex)."""
135+
if config_path.exists():
136+
with open(config_path) as f:
137+
config_data = tomlkit.load(f)
138+
else:
139+
config_data = tomlkit.document()
140+
141+
if "mcp_servers" not in config_data:
142+
config_data["mcp_servers"] = tomlkit.table(is_super_table=True)
143+
144+
mcp_servers = config_data["mcp_servers"]
145+
assert isinstance(mcp_servers, dict)
146+
mcp_servers[server_name] = server_config
147+
148+
config_path.parent.mkdir(parents=True, exist_ok=True)
149+
with open(config_path, "w") as f:
150+
tomlkit.dump(config_data, f)
151+
152+
153+
def update_mcp_config(
154+
client_name: str, config_path: Path, server_name: str, server_config: dict
155+
) -> None:
156+
"""Write an MCP server entry to the appropriate config file format."""
157+
if client_name == "codex":
158+
write_toml_mcp_config(config_path, server_name, server_config)
159+
else:
160+
write_json_mcp_config(config_path, server_name, server_config)
161+
162+
91163
@serve_app.command("agent")
92164
def serve_agent(
93165
namespace: Annotated[
@@ -168,43 +240,43 @@ def serve_agent(
168240
get_logger().error("No .cursor directory found")
169241
raise typer.Exit(1)
170242
configs_to_update = [("cursor", cursor_paths[0])]
243+
case RegisterMode.CODEX:
244+
codex_paths = get_codex_config_paths()
245+
if not codex_paths:
246+
# Explicit --register codex: create .codex/ dir
247+
git_root = find_git_root()
248+
base = git_root if git_root else Path(".")
249+
codex_paths = [base / ".codex" / "config.toml"]
250+
configs_to_update = [("codex", codex_paths[0])]
171251
case RegisterMode.AUTO:
172252
configs_to_update = find_mcp_config_files()
173253
if not configs_to_update:
174254
get_logger().error("No MCP config files found")
175255
raise typer.Exit(1)
176256

257+
# Build server config
258+
server_config = {
259+
"command": "dispatch",
260+
"args": [
261+
"mcp",
262+
"serve",
263+
"agent",
264+
"--namespace",
265+
namespace,
266+
"--agent",
267+
agent,
268+
]
269+
+ (["--experimental-tasks"] if experimental_tasks else []),
270+
}
271+
177272
# Update configs
178273
for client_name, config_path in configs_to_update:
179-
if config_path.exists():
180-
with open(config_path) as f:
181-
config_data = json.load(f)
274+
if client_name == "codex":
275+
server_name = f"dispatch_agent_{namespace}_{agent}"
182276
else:
183-
config_data = {}
184-
185-
if "mcpServers" not in config_data:
186-
config_data["mcpServers"] = {}
187-
188-
server_name = f"dispatch-agent-{namespace}-{agent}"
189-
config_data["mcpServers"][server_name] = {
190-
"command": "dispatch",
191-
"args": [
192-
"mcp",
193-
"serve",
194-
"agent",
195-
"--namespace",
196-
namespace,
197-
"--agent",
198-
agent,
199-
]
200-
+ (["--experimental-tasks"] if experimental_tasks else []),
201-
}
202-
203-
config_path.parent.mkdir(parents=True, exist_ok=True)
204-
with open(config_path, "w") as f:
205-
json.dump(config_data, f, indent=2)
206-
f.write("\n")
277+
server_name = f"dispatch-agent-{namespace}-{agent}"
207278

279+
update_mcp_config(client_name, config_path, server_name, server_config)
208280
get_logger().success(f"Updated {client_name} config: {config_path}")
209281

210282
get_logger().info("")
@@ -294,43 +366,47 @@ def serve_operator(
294366
get_logger().error("No .cursor directory found")
295367
raise typer.Exit(1)
296368
configs_to_update = [("cursor", cursor_paths[0])]
369+
case RegisterMode.CODEX:
370+
codex_paths = get_codex_config_paths()
371+
if not codex_paths:
372+
# Explicit --register codex: create .codex/ dir
373+
git_root = find_git_root()
374+
base = git_root if git_root else Path(".")
375+
codex_paths = [base / ".codex" / "config.toml"]
376+
configs_to_update = [("codex", codex_paths[0])]
297377
case RegisterMode.AUTO:
298378
configs_to_update = find_mcp_config_files()
299379
if not configs_to_update:
300380
get_logger().error("No MCP config files found")
301381
raise typer.Exit(1)
302382

383+
# Build server config
384+
args = ["mcp", "serve", "operator"]
385+
if namespace:
386+
args.extend(["--namespace", namespace])
387+
388+
server_config = {
389+
"command": "dispatch",
390+
"args": args,
391+
}
392+
303393
# Update configs
304394
for client_name, config_path in configs_to_update:
305-
if config_path.exists():
306-
with open(config_path) as f:
307-
config_data = json.load(f)
395+
if client_name == "codex":
396+
server_name = (
397+
f"dispatch_operator_{namespace}"
398+
if namespace
399+
else "dispatch_operator"
400+
)
308401
else:
309-
config_data = {}
310-
311-
if "mcpServers" not in config_data:
312-
config_data["mcpServers"] = {}
313-
314-
server_name = (
315-
f"dispatch-operator-{namespace}"
316-
if namespace
317-
else "dispatch-operator"
318-
)
319-
args = ["mcp", "serve", "operator"]
320-
if namespace:
321-
args.extend(["--namespace", namespace])
322-
323-
config_data["mcpServers"][server_name] = {
324-
"command": "dispatch",
325-
"args": args,
326-
}
327-
328-
config_path.parent.mkdir(parents=True, exist_ok=True)
329-
with open(config_path, "w") as f:
330-
json.dump(config_data, f, indent=2)
331-
f.write("\n")
332-
333-
print(f"[green]✓[/green] Updated {client_name} config: {config_path}")
402+
server_name = (
403+
f"dispatch-operator-{namespace}"
404+
if namespace
405+
else "dispatch-operator"
406+
)
407+
408+
update_mcp_config(client_name, config_path, server_name, server_config)
409+
get_logger().success(f"Updated {client_name} config: {config_path}")
334410

335411
get_logger().info("")
336412
get_logger().success("Operator MCP server registered")

dispatch_cli/mcp/operator/prompts/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)