cacli 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.
- cacli-0.1.0/LICENSE +21 -0
- cacli-0.1.0/PKG-INFO +15 -0
- cacli-0.1.0/README.md +2 -0
- cacli-0.1.0/pyproject.toml +26 -0
- cacli-0.1.0/setup.cfg +4 -0
- cacli-0.1.0/src/cacli/__init__.py +28 -0
- cacli-0.1.0/src/cacli/cli.py +195 -0
- cacli-0.1.0/src/cacli/providers/__init__.py +33 -0
- cacli-0.1.0/src/cacli/providers/base.py +52 -0
- cacli-0.1.0/src/cacli/providers/claude.py +129 -0
- cacli-0.1.0/src/cacli/providers/codex.py +111 -0
- cacli-0.1.0/src/cacli/providers/cursor.py +140 -0
- cacli-0.1.0/src/cacli/providers/gemini.py +111 -0
- cacli-0.1.0/src/cacli/runner.py +82 -0
- cacli-0.1.0/src/cacli/sessions.py +111 -0
- cacli-0.1.0/src/cacli/spawn.py +98 -0
- cacli-0.1.0/src/cacli/status.py +208 -0
- cacli-0.1.0/src/cacli/types.py +32 -0
- cacli-0.1.0/src/cacli.egg-info/PKG-INFO +15 -0
- cacli-0.1.0/src/cacli.egg-info/SOURCES.txt +21 -0
- cacli-0.1.0/src/cacli.egg-info/dependency_links.txt +1 -0
- cacli-0.1.0/src/cacli.egg-info/entry_points.txt +2 -0
- cacli-0.1.0/src/cacli.egg-info/top_level.txt +1 -0
cacli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Taylor AI
|
|
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.
|
cacli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cacli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Provider-agnostic CLI for running coding agents
|
|
5
|
+
Author: Benjamin Anderson
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/taylorai/cacli
|
|
8
|
+
Project-URL: Repository, https://github.com/taylorai/cacli
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
14
|
+
# cacli
|
|
15
|
+
coding agent CLI
|
cacli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cacli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Provider-agnostic CLI for running coding agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Benjamin Anderson" }]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
cacli = "cacli.cli:main"
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/taylorai/cacli"
|
|
19
|
+
Repository = "https://github.com/taylorai/cacli"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.ruff.lint]
|
|
25
|
+
select = ["E4", "E7", "E9", "F", "B"]
|
|
26
|
+
ignore = ["B008", "B904"]
|
cacli-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cacli — Coding Agent CLI.
|
|
3
|
+
|
|
4
|
+
Provider-agnostic interface for running headless coding agents.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from cacli.providers import get_provider, list_providers
|
|
8
|
+
from cacli.runner import (
|
|
9
|
+
build_command,
|
|
10
|
+
build_initial_log_entry,
|
|
11
|
+
parse_output,
|
|
12
|
+
run_agent,
|
|
13
|
+
)
|
|
14
|
+
from cacli.sessions import SessionInfo
|
|
15
|
+
from cacli.types import AgentRunResult, ExecResult, ShellExecFn
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"run_agent",
|
|
19
|
+
"build_command",
|
|
20
|
+
"parse_output",
|
|
21
|
+
"build_initial_log_entry",
|
|
22
|
+
"get_provider",
|
|
23
|
+
"list_providers",
|
|
24
|
+
"AgentRunResult",
|
|
25
|
+
"ExecResult",
|
|
26
|
+
"ShellExecFn",
|
|
27
|
+
"SessionInfo",
|
|
28
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""CLI entrypoint for cacli."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from cacli.providers import list_providers
|
|
10
|
+
from cacli.runner import build_command, run_agent
|
|
11
|
+
from cacli.types import ExecResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def subprocess_exec(
|
|
15
|
+
cmd: str, cwd: str, timeout: int, env: dict[str, str] | None
|
|
16
|
+
) -> ExecResult:
|
|
17
|
+
"""Default exec_fn using subprocess."""
|
|
18
|
+
full_env = os.environ.copy()
|
|
19
|
+
if env:
|
|
20
|
+
full_env.update(env)
|
|
21
|
+
try:
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
cmd,
|
|
24
|
+
shell=True,
|
|
25
|
+
capture_output=True,
|
|
26
|
+
text=True,
|
|
27
|
+
cwd=cwd,
|
|
28
|
+
timeout=timeout,
|
|
29
|
+
env=full_env,
|
|
30
|
+
)
|
|
31
|
+
return ExecResult(
|
|
32
|
+
exit_code=result.returncode,
|
|
33
|
+
stdout=result.stdout,
|
|
34
|
+
stderr=result.stderr,
|
|
35
|
+
)
|
|
36
|
+
except subprocess.TimeoutExpired:
|
|
37
|
+
return ExecResult(exit_code=124, stdout="", stderr="Command timed out")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _normalize_argv(argv: list[str]) -> list[str]:
|
|
41
|
+
"""Prepend 'run' if first arg isn't a known subcommand.
|
|
42
|
+
|
|
43
|
+
Preserves backward compat: ``cacli "my prompt"`` works as ``cacli run "my prompt"``.
|
|
44
|
+
"""
|
|
45
|
+
if not argv:
|
|
46
|
+
return argv
|
|
47
|
+
first = argv[0]
|
|
48
|
+
known = {"run", "spawn", "status"}
|
|
49
|
+
if first in known or first in ("-h", "--help", "help") or first.startswith("-"):
|
|
50
|
+
return argv
|
|
51
|
+
return ["run", *argv]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _add_common_args(parser: argparse.ArgumentParser) -> None:
|
|
55
|
+
"""Add arguments shared between run and spawn."""
|
|
56
|
+
parser.add_argument("prompt", help="The task prompt")
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--provider",
|
|
59
|
+
"-p",
|
|
60
|
+
default="claude",
|
|
61
|
+
choices=list_providers(),
|
|
62
|
+
help="Agent provider (default: claude)",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument("--model", "-m", default=None, help="Model name")
|
|
65
|
+
parser.add_argument("--reasoning-effort", "-e", default=None)
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
"--settings",
|
|
68
|
+
"-s",
|
|
69
|
+
default=None,
|
|
70
|
+
help="Path to settings file (forwarded to claude --settings)",
|
|
71
|
+
)
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--no-web-search",
|
|
74
|
+
dest="web_search",
|
|
75
|
+
action="store_false",
|
|
76
|
+
default=True,
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument("--cwd", default=".", help="Working directory")
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--timeout", "-t", type=int, default=3600, help="Timeout in seconds"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _run_command(args) -> None:
|
|
85
|
+
"""Handle 'cacli run' — synchronous agent execution."""
|
|
86
|
+
if args.command_only:
|
|
87
|
+
cmd = build_command(
|
|
88
|
+
args.provider,
|
|
89
|
+
args.prompt,
|
|
90
|
+
model=args.model,
|
|
91
|
+
reasoning_effort=args.reasoning_effort,
|
|
92
|
+
web_search=args.web_search,
|
|
93
|
+
settings=args.settings,
|
|
94
|
+
)
|
|
95
|
+
print(cmd)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
result = run_agent(
|
|
99
|
+
provider=args.provider,
|
|
100
|
+
prompt=args.prompt,
|
|
101
|
+
exec_fn=subprocess_exec,
|
|
102
|
+
model=args.model,
|
|
103
|
+
reasoning_effort=args.reasoning_effort,
|
|
104
|
+
web_search=args.web_search,
|
|
105
|
+
settings=args.settings,
|
|
106
|
+
cwd=args.cwd,
|
|
107
|
+
timeout=args.timeout,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if args.raw:
|
|
111
|
+
print(result.raw_output)
|
|
112
|
+
elif args.json:
|
|
113
|
+
print(
|
|
114
|
+
json.dumps(
|
|
115
|
+
{
|
|
116
|
+
"exit_code": result.exit_code,
|
|
117
|
+
"result_message": result.result_message,
|
|
118
|
+
"total_cost": result.total_cost,
|
|
119
|
+
"provider": result.provider,
|
|
120
|
+
"model": result.model,
|
|
121
|
+
"command": result.command,
|
|
122
|
+
},
|
|
123
|
+
indent=2,
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
if result.result_message:
|
|
128
|
+
print(result.result_message)
|
|
129
|
+
else:
|
|
130
|
+
print(result.raw_output)
|
|
131
|
+
|
|
132
|
+
sys.exit(result.exit_code)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _spawn_command(args) -> None:
|
|
136
|
+
"""Handle 'cacli spawn' — launch agent in tmux."""
|
|
137
|
+
from cacli.spawn import spawn_agent
|
|
138
|
+
|
|
139
|
+
spawn_agent(args)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _status_command(_args) -> None:
|
|
143
|
+
"""Handle 'cacli status' — TUI dashboard."""
|
|
144
|
+
from cacli.status import status_tui
|
|
145
|
+
|
|
146
|
+
status_tui()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def main() -> None:
|
|
150
|
+
parser = argparse.ArgumentParser(
|
|
151
|
+
prog="cacli",
|
|
152
|
+
description="Run coding agents (provider-agnostic)",
|
|
153
|
+
)
|
|
154
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
155
|
+
|
|
156
|
+
# cacli run <prompt>
|
|
157
|
+
run_parser = subparsers.add_parser("run", help="Run agent synchronously")
|
|
158
|
+
_add_common_args(run_parser)
|
|
159
|
+
run_parser.add_argument(
|
|
160
|
+
"--raw",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="Print raw output instead of parsed result",
|
|
163
|
+
)
|
|
164
|
+
run_parser.add_argument(
|
|
165
|
+
"--command-only",
|
|
166
|
+
action="store_true",
|
|
167
|
+
help="Just print the command, don't execute",
|
|
168
|
+
)
|
|
169
|
+
run_parser.add_argument("--json", action="store_true", help="Output result as JSON")
|
|
170
|
+
|
|
171
|
+
# cacli spawn <prompt>
|
|
172
|
+
spawn_parser = subparsers.add_parser("spawn", help="Spawn agent in tmux session")
|
|
173
|
+
_add_common_args(spawn_parser)
|
|
174
|
+
spawn_parser.add_argument(
|
|
175
|
+
"--name", "-n", default=None, help="Human-readable session name"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# cacli status
|
|
179
|
+
subparsers.add_parser("status", help="Show spawned agent dashboard")
|
|
180
|
+
|
|
181
|
+
args = parser.parse_args(_normalize_argv(sys.argv[1:]))
|
|
182
|
+
|
|
183
|
+
if args.command is None:
|
|
184
|
+
parser.print_help()
|
|
185
|
+
sys.exit(1)
|
|
186
|
+
elif args.command == "run":
|
|
187
|
+
_run_command(args)
|
|
188
|
+
elif args.command == "spawn":
|
|
189
|
+
_spawn_command(args)
|
|
190
|
+
elif args.command == "status":
|
|
191
|
+
_status_command(args)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
if __name__ == "__main__":
|
|
195
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Provider registry."""
|
|
2
|
+
|
|
3
|
+
from cacli.providers.base import BaseProvider
|
|
4
|
+
from cacli.providers.claude import ClaudeProvider
|
|
5
|
+
from cacli.providers.codex import CodexProvider
|
|
6
|
+
from cacli.providers.cursor import CursorProvider
|
|
7
|
+
from cacli.providers.gemini import GeminiProvider
|
|
8
|
+
|
|
9
|
+
_PROVIDERS: dict[str, BaseProvider] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _init_registry() -> None:
|
|
13
|
+
_PROVIDERS["claude"] = ClaudeProvider()
|
|
14
|
+
_PROVIDERS["codex"] = CodexProvider()
|
|
15
|
+
_PROVIDERS["openai"] = CodexProvider()
|
|
16
|
+
_PROVIDERS["gemini"] = GeminiProvider()
|
|
17
|
+
_PROVIDERS["cursor"] = CursorProvider()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_init_registry()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_provider(name: str) -> BaseProvider:
|
|
24
|
+
"""Get a provider instance by name."""
|
|
25
|
+
if name not in _PROVIDERS:
|
|
26
|
+
available = ", ".join(sorted(_PROVIDERS))
|
|
27
|
+
raise ValueError(f"Unknown provider: {name}. Available: {available}")
|
|
28
|
+
return _PROVIDERS[name]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def list_providers() -> list[str]:
|
|
32
|
+
"""List all available provider names."""
|
|
33
|
+
return sorted(_PROVIDERS.keys())
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Abstract base class for coding agent providers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
from cacli.types import AgentRunResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseProvider(ABC):
|
|
9
|
+
"""Abstract base for all coding agent providers."""
|
|
10
|
+
|
|
11
|
+
name: str
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def build_command(
|
|
15
|
+
self,
|
|
16
|
+
prompt: str,
|
|
17
|
+
model: str | None = None,
|
|
18
|
+
reasoning_effort: str | None = None,
|
|
19
|
+
web_search: bool = True,
|
|
20
|
+
settings: str | None = None,
|
|
21
|
+
) -> str:
|
|
22
|
+
"""Build the shell command string to invoke this provider."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def parse_output(self, raw_output: str) -> AgentRunResult:
|
|
27
|
+
"""Parse raw output and extract result message, cost, etc."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def build_initial_log_entry(self, prompt: str, model: str | None = None) -> str:
|
|
32
|
+
"""Build the initial JSONL log entry for this provider's format."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def build_pr_description_command(
|
|
37
|
+
self,
|
|
38
|
+
prompt: str,
|
|
39
|
+
model: str | None = None,
|
|
40
|
+
settings: str | None = None,
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Build command for generating PR descriptions."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def extract_result_from_json(self, raw_output: str) -> str:
|
|
47
|
+
"""Extract the result text from JSON output."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def resolve_model(self, model: str | None) -> str | None:
|
|
51
|
+
"""Resolve model aliases. Override in providers with aliases."""
|
|
52
|
+
return model
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Claude Code provider."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
from cacli.providers.base import BaseProvider
|
|
7
|
+
from cacli.types import AgentRunResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ClaudeProvider(BaseProvider):
|
|
11
|
+
"""Provider for the Claude Code CLI."""
|
|
12
|
+
|
|
13
|
+
name = "claude"
|
|
14
|
+
|
|
15
|
+
def build_command(
|
|
16
|
+
self,
|
|
17
|
+
prompt: str,
|
|
18
|
+
model: str | None = None,
|
|
19
|
+
reasoning_effort: str | None = None,
|
|
20
|
+
web_search: bool = True,
|
|
21
|
+
settings: str | None = None,
|
|
22
|
+
) -> str:
|
|
23
|
+
safe_prompt = shlex.quote(prompt)
|
|
24
|
+
cmd = f"claude -p {safe_prompt} --verbose --output-format=stream-json"
|
|
25
|
+
if model:
|
|
26
|
+
cmd += f" --model {shlex.quote(model)}"
|
|
27
|
+
if settings:
|
|
28
|
+
cmd += f" --settings {shlex.quote(settings)}"
|
|
29
|
+
if not web_search:
|
|
30
|
+
cmd += " --disallowedTools WebFetch --disallowedTools WebSearch"
|
|
31
|
+
return cmd
|
|
32
|
+
|
|
33
|
+
def parse_output(self, raw_output: str) -> AgentRunResult:
|
|
34
|
+
result_message = ""
|
|
35
|
+
total_cost = None
|
|
36
|
+
permission_denials = []
|
|
37
|
+
|
|
38
|
+
for line in reversed(raw_output.strip().split("\n")):
|
|
39
|
+
if not line.strip():
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
entry = json.loads(line)
|
|
43
|
+
entries = entry if isinstance(entry, list) else [entry]
|
|
44
|
+
for item in reversed(entries):
|
|
45
|
+
if not isinstance(item, dict):
|
|
46
|
+
continue
|
|
47
|
+
if item.get("type") == "result":
|
|
48
|
+
result_message = item.get("result", "")
|
|
49
|
+
total_cost = item.get("total_cost_usd")
|
|
50
|
+
permission_denials = item.get("permission_denials", [])
|
|
51
|
+
break
|
|
52
|
+
if result_message:
|
|
53
|
+
break
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
return AgentRunResult(
|
|
58
|
+
result_message=result_message,
|
|
59
|
+
total_cost=total_cost,
|
|
60
|
+
permission_denials=permission_denials,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def build_initial_log_entry(self, prompt: str, model: str | None = None) -> str:
|
|
64
|
+
return json.dumps(
|
|
65
|
+
{
|
|
66
|
+
"type": "user",
|
|
67
|
+
"message": {
|
|
68
|
+
"role": "user",
|
|
69
|
+
"content": [{"type": "text", "text": prompt}],
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def build_pr_description_command(
|
|
75
|
+
self,
|
|
76
|
+
prompt: str,
|
|
77
|
+
model: str | None = None,
|
|
78
|
+
settings: str | None = None,
|
|
79
|
+
) -> str:
|
|
80
|
+
safe_prompt = shlex.quote(prompt)
|
|
81
|
+
cmd = f"claude -p {safe_prompt} --verbose --output-format json"
|
|
82
|
+
if model:
|
|
83
|
+
cmd += f" --model {shlex.quote(model)}"
|
|
84
|
+
if settings:
|
|
85
|
+
cmd += f" --settings {shlex.quote(settings)}"
|
|
86
|
+
return cmd
|
|
87
|
+
|
|
88
|
+
def extract_result_from_json(self, raw_output: str) -> str:
|
|
89
|
+
# First try single-object JSON with "result" key
|
|
90
|
+
try:
|
|
91
|
+
data = json.loads(raw_output)
|
|
92
|
+
if isinstance(data, dict) and "result" in data:
|
|
93
|
+
return data["result"]
|
|
94
|
+
if isinstance(data, list):
|
|
95
|
+
for item in reversed(data):
|
|
96
|
+
if isinstance(item, dict) and "result" in item:
|
|
97
|
+
return item["result"]
|
|
98
|
+
except json.JSONDecodeError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
# Try JSONL: extract result from stream-json format
|
|
102
|
+
# (same logic as parse_output)
|
|
103
|
+
for line in reversed(raw_output.strip().split("\n")):
|
|
104
|
+
line = line.strip()
|
|
105
|
+
if not line:
|
|
106
|
+
continue
|
|
107
|
+
try:
|
|
108
|
+
entry = json.loads(line)
|
|
109
|
+
entries = entry if isinstance(entry, list) else [entry]
|
|
110
|
+
for item in reversed(entries):
|
|
111
|
+
if not isinstance(item, dict):
|
|
112
|
+
continue
|
|
113
|
+
if item.get("type") == "result":
|
|
114
|
+
return item.get("result", "")
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
# Fallback: look for any line with "result" key
|
|
119
|
+
for line in raw_output.split("\n"):
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if line.startswith("{") and "result" in line:
|
|
122
|
+
try:
|
|
123
|
+
data = json.loads(line)
|
|
124
|
+
if isinstance(data, dict) and "result" in data:
|
|
125
|
+
return data["result"]
|
|
126
|
+
except json.JSONDecodeError:
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
return raw_output
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Codex/OpenAI provider."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shlex
|
|
5
|
+
|
|
6
|
+
from cacli.providers.base import BaseProvider
|
|
7
|
+
from cacli.types import AgentRunResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CodexProvider(BaseProvider):
|
|
11
|
+
"""Provider for Codex/OpenAI CLI."""
|
|
12
|
+
|
|
13
|
+
name = "codex"
|
|
14
|
+
|
|
15
|
+
def build_command(
|
|
16
|
+
self,
|
|
17
|
+
prompt: str,
|
|
18
|
+
model: str | None = None,
|
|
19
|
+
reasoning_effort: str | None = None,
|
|
20
|
+
web_search: bool = True,
|
|
21
|
+
settings: str | None = None,
|
|
22
|
+
) -> str:
|
|
23
|
+
safe_prompt = shlex.quote(prompt)
|
|
24
|
+
cmd = f"codex exec {safe_prompt} --sandbox danger-full-access --json --skip-git-repo-check"
|
|
25
|
+
if model:
|
|
26
|
+
cmd += f" --model {shlex.quote(model)}"
|
|
27
|
+
if reasoning_effort is not None:
|
|
28
|
+
cmd += f" --config model_reasoning_effort={reasoning_effort}"
|
|
29
|
+
if web_search:
|
|
30
|
+
cmd += " --config features.web_search_request=true"
|
|
31
|
+
return cmd
|
|
32
|
+
|
|
33
|
+
def parse_output(self, raw_output: str) -> AgentRunResult:
|
|
34
|
+
result_message = ""
|
|
35
|
+
|
|
36
|
+
for line in reversed(raw_output.strip().split("\n")):
|
|
37
|
+
if not line.strip():
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
entry = json.loads(line)
|
|
41
|
+
if entry.get("type") == "item.completed":
|
|
42
|
+
item = entry.get("item", {})
|
|
43
|
+
if item.get("type") == "agent_message":
|
|
44
|
+
result_message = item.get("text", "")
|
|
45
|
+
break
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
return AgentRunResult(result_message=result_message)
|
|
50
|
+
|
|
51
|
+
def build_initial_log_entry(self, prompt: str, model: str | None = None) -> str:
|
|
52
|
+
item = {
|
|
53
|
+
"id": "item_user",
|
|
54
|
+
"type": "user_message",
|
|
55
|
+
"text": prompt,
|
|
56
|
+
}
|
|
57
|
+
if model:
|
|
58
|
+
item["model"] = model
|
|
59
|
+
return json.dumps({"type": "item.completed", "item": item})
|
|
60
|
+
|
|
61
|
+
def build_pr_description_command(
|
|
62
|
+
self,
|
|
63
|
+
prompt: str,
|
|
64
|
+
model: str | None = None,
|
|
65
|
+
settings: str | None = None,
|
|
66
|
+
) -> str:
|
|
67
|
+
safe_prompt = shlex.quote(prompt)
|
|
68
|
+
cmd = f"codex exec {safe_prompt} --sandbox danger-full-access --json --skip-git-repo-check"
|
|
69
|
+
if model:
|
|
70
|
+
cmd += f" --model {shlex.quote(model)}"
|
|
71
|
+
return cmd
|
|
72
|
+
|
|
73
|
+
def extract_result_from_json(self, raw_output: str) -> str:
|
|
74
|
+
# First try single-object JSON with "result" key
|
|
75
|
+
try:
|
|
76
|
+
data = json.loads(raw_output)
|
|
77
|
+
if isinstance(data, dict) and "result" in data:
|
|
78
|
+
return data["result"]
|
|
79
|
+
except json.JSONDecodeError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Try JSONL: extract last agent_message from item.completed events
|
|
83
|
+
# (same logic as parse_output)
|
|
84
|
+
for line in reversed(raw_output.strip().split("\n")):
|
|
85
|
+
line = line.strip()
|
|
86
|
+
if not line:
|
|
87
|
+
continue
|
|
88
|
+
try:
|
|
89
|
+
entry = json.loads(line)
|
|
90
|
+
if entry.get("type") == "item.completed":
|
|
91
|
+
item = entry.get("item", {})
|
|
92
|
+
if item.get("type") == "agent_message":
|
|
93
|
+
return item.get("text", "")
|
|
94
|
+
except json.JSONDecodeError:
|
|
95
|
+
continue
|
|
96
|
+
|
|
97
|
+
# Fallback: look for any line with "result" key
|
|
98
|
+
for line in raw_output.split("\n"):
|
|
99
|
+
line = line.strip()
|
|
100
|
+
if line.startswith("{") and "result" in line:
|
|
101
|
+
try:
|
|
102
|
+
data = json.loads(line)
|
|
103
|
+
if "result" in data:
|
|
104
|
+
return data["result"]
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
return raw_output
|
|
109
|
+
|
|
110
|
+
def skills_dir(self) -> str:
|
|
111
|
+
return "/root/.codex/skills"
|