juno-code 1.0.15 → 1.0.17
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.
- package/dist/bin/cli.js +6927 -3937
- package/dist/bin/cli.js.map +1 -1
- package/dist/bin/cli.mjs +6926 -3937
- package/dist/bin/cli.mjs.map +1 -1
- package/dist/index.js +43 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43 -15
- package/dist/index.mjs.map +1 -1
- package/dist/templates/scripts/kanban.sh +160 -6
- package/dist/templates/services/README.md +271 -0
- package/dist/templates/services/claude.py +397 -0
- package/dist/templates/services/codex.py +272 -0
- package/package.json +73 -8
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Claude Service Script for juno-code
|
|
4
|
+
This script provides a wrapper around Anthropic Claude CLI with configurable options.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, List, Dict, Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClaudeService:
|
|
18
|
+
"""Service wrapper for Anthropic Claude CLI"""
|
|
19
|
+
|
|
20
|
+
# Default configuration
|
|
21
|
+
DEFAULT_MODEL = "claude-sonnet-4-5-20250929"
|
|
22
|
+
DEFAULT_AUTO_INSTRUCTION = """You are Claude Code, an AI coding assistant. Follow the instructions provided and generate high-quality code."""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.model_name = self.DEFAULT_MODEL
|
|
26
|
+
self.auto_instruction = self.DEFAULT_AUTO_INSTRUCTION
|
|
27
|
+
self.project_path = os.getcwd()
|
|
28
|
+
self.prompt = ""
|
|
29
|
+
self.additional_args: List[str] = []
|
|
30
|
+
self.message_counter = 0
|
|
31
|
+
self.verbose = False
|
|
32
|
+
|
|
33
|
+
def check_claude_installed(self) -> bool:
|
|
34
|
+
"""Check if claude CLI is installed and available"""
|
|
35
|
+
try:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["which", "claude"],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
check=False
|
|
41
|
+
)
|
|
42
|
+
return result.returncode == 0
|
|
43
|
+
except Exception:
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
def parse_arguments(self) -> argparse.Namespace:
|
|
47
|
+
"""Parse command line arguments"""
|
|
48
|
+
parser = argparse.ArgumentParser(
|
|
49
|
+
description="Claude Service - Wrapper for Anthropic Claude CLI",
|
|
50
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
51
|
+
epilog="""
|
|
52
|
+
Examples:
|
|
53
|
+
%(prog)s -p "Write a hello world function"
|
|
54
|
+
%(prog)s -pp prompt.txt --cd /path/to/project
|
|
55
|
+
%(prog)s -p "Add tests" -m claude-opus-4-20250514 --tool "Bash Edit"
|
|
56
|
+
"""
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Core arguments
|
|
60
|
+
prompt_group = parser.add_mutually_exclusive_group(required=False)
|
|
61
|
+
prompt_group.add_argument(
|
|
62
|
+
"-p", "--prompt",
|
|
63
|
+
type=str,
|
|
64
|
+
help="Prompt text to send to claude"
|
|
65
|
+
)
|
|
66
|
+
prompt_group.add_argument(
|
|
67
|
+
"-pp", "--prompt-file",
|
|
68
|
+
type=str,
|
|
69
|
+
help="Path to file containing the prompt"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--cd",
|
|
74
|
+
type=str,
|
|
75
|
+
default=os.getcwd(),
|
|
76
|
+
help="Project path (absolute path). Default: current directory"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"-m", "--model",
|
|
81
|
+
type=str,
|
|
82
|
+
default=self.DEFAULT_MODEL,
|
|
83
|
+
help=f"Model name (e.g. 'sonnet', 'opus', or full name). Default: {self.DEFAULT_MODEL}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--auto-instruction",
|
|
88
|
+
type=str,
|
|
89
|
+
default=self.DEFAULT_AUTO_INSTRUCTION,
|
|
90
|
+
help="Auto instruction to prepend to prompt"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--tool",
|
|
95
|
+
action="append",
|
|
96
|
+
dest="allowed_tools",
|
|
97
|
+
help="Allowed tools (can be used multiple times, e.g. 'Bash' 'Edit')"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--permission-mode",
|
|
102
|
+
type=str,
|
|
103
|
+
choices=["acceptEdits", "bypassPermissions", "default", "plan"],
|
|
104
|
+
default="bypassPermissions",
|
|
105
|
+
help="Permission mode for the session. Default: bypassPermissions"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"--json",
|
|
110
|
+
action="store_true",
|
|
111
|
+
default=True,
|
|
112
|
+
help="Output in JSON format (default: True)"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--pretty",
|
|
117
|
+
type=str,
|
|
118
|
+
default=os.environ.get("CLAUDE_PRETTY", "true").lower(),
|
|
119
|
+
choices=["true", "false"],
|
|
120
|
+
help="Pretty print JSON output (default: true, env: CLAUDE_PRETTY)"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--verbose",
|
|
125
|
+
action="store_true",
|
|
126
|
+
help="Enable verbose output"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"-c", "--continue",
|
|
131
|
+
action="store_true",
|
|
132
|
+
dest="continue_conversation",
|
|
133
|
+
help="Continue the most recent conversation"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
parser.add_argument(
|
|
137
|
+
"--additional-args",
|
|
138
|
+
type=str,
|
|
139
|
+
help="Additional claude arguments as a space-separated string"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return parser.parse_args()
|
|
143
|
+
|
|
144
|
+
def read_prompt_file(self, file_path: str) -> str:
|
|
145
|
+
"""Read prompt from a file"""
|
|
146
|
+
try:
|
|
147
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
148
|
+
return f.read().strip()
|
|
149
|
+
except FileNotFoundError:
|
|
150
|
+
print(f"Error: Prompt file not found: {file_path}", file=sys.stderr)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Error reading prompt file: {e}", file=sys.stderr)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
def build_claude_command(self, args: argparse.Namespace) -> List[str]:
|
|
157
|
+
"""Build the claude command with all arguments"""
|
|
158
|
+
# Start with base command
|
|
159
|
+
cmd = [
|
|
160
|
+
"claude",
|
|
161
|
+
"--print", # Non-interactive mode
|
|
162
|
+
"--model", self.model_name,
|
|
163
|
+
"--permission-mode", args.permission_mode,
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
# Build the full prompt (auto_instruction + user prompt)
|
|
167
|
+
# IMPORTANT: Prompt must come BEFORE --allowed-tools
|
|
168
|
+
# because --allowed-tools consumes all following arguments as tool names
|
|
169
|
+
full_prompt = f"{self.auto_instruction}\n\n{self.prompt}"
|
|
170
|
+
cmd.append(full_prompt)
|
|
171
|
+
|
|
172
|
+
# Add allowed tools if specified (AFTER the prompt)
|
|
173
|
+
if args.allowed_tools:
|
|
174
|
+
cmd.append("--allowed-tools")
|
|
175
|
+
cmd.extend(args.allowed_tools)
|
|
176
|
+
else:
|
|
177
|
+
# Default allowed tools similar to claude_code.py
|
|
178
|
+
default_tools = [
|
|
179
|
+
"Read", "Write", "Edit", "MultiEdit",
|
|
180
|
+
"Bash", "Glob", "Grep", "WebFetch",
|
|
181
|
+
"WebSearch", "TodoWrite"
|
|
182
|
+
]
|
|
183
|
+
cmd.append("--allowed-tools")
|
|
184
|
+
cmd.extend(default_tools)
|
|
185
|
+
|
|
186
|
+
# Add continue flag if specified
|
|
187
|
+
if args.continue_conversation:
|
|
188
|
+
cmd.append("--continue")
|
|
189
|
+
|
|
190
|
+
# Add output format if JSON requested
|
|
191
|
+
# Note: stream-json requires --verbose when using --print mode
|
|
192
|
+
if args.json:
|
|
193
|
+
cmd.extend(["--output-format", "stream-json", "--verbose"])
|
|
194
|
+
|
|
195
|
+
# Add any additional arguments
|
|
196
|
+
if args.additional_args:
|
|
197
|
+
additional = args.additional_args.split()
|
|
198
|
+
cmd.extend(additional)
|
|
199
|
+
|
|
200
|
+
return cmd
|
|
201
|
+
|
|
202
|
+
def pretty_format_json(self, json_line: str) -> Optional[str]:
|
|
203
|
+
"""
|
|
204
|
+
Format JSON line for pretty output.
|
|
205
|
+
For type=assistant: show datetime, message content, and counter
|
|
206
|
+
For other types: show full message with datetime and counter
|
|
207
|
+
Returns None if line should be skipped
|
|
208
|
+
|
|
209
|
+
IMPORTANT: Always preserve the 'type' field so shell backend can parse events
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
data = json.loads(json_line)
|
|
213
|
+
self.message_counter += 1
|
|
214
|
+
|
|
215
|
+
# Get current datetime in readable format
|
|
216
|
+
now = datetime.now().strftime("%I:%M:%S %p")
|
|
217
|
+
|
|
218
|
+
# For assistant messages, show simplified output
|
|
219
|
+
if data.get("type") == "assistant":
|
|
220
|
+
message = data.get("message", {})
|
|
221
|
+
content_list = message.get("content", [])
|
|
222
|
+
|
|
223
|
+
# Extract text content or tool_use from content array
|
|
224
|
+
text_content = ""
|
|
225
|
+
tool_use_data = None
|
|
226
|
+
|
|
227
|
+
for item in content_list:
|
|
228
|
+
if isinstance(item, dict):
|
|
229
|
+
if item.get("type") == "text":
|
|
230
|
+
text_content = item.get("text", "")
|
|
231
|
+
break
|
|
232
|
+
elif item.get("type") == "tool_use":
|
|
233
|
+
# Extract tool name and input for tool_use
|
|
234
|
+
tool_use_data = {
|
|
235
|
+
"name": item.get("name", ""),
|
|
236
|
+
"input": item.get("input", {})
|
|
237
|
+
}
|
|
238
|
+
break
|
|
239
|
+
|
|
240
|
+
# Create simplified output with datetime, content/tool_use, and counter
|
|
241
|
+
# KEEP the 'type' field for shell backend compatibility
|
|
242
|
+
simplified = {
|
|
243
|
+
"type": "assistant",
|
|
244
|
+
"datetime": now,
|
|
245
|
+
"counter": f"#{self.message_counter}"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Add either content or tool_use data
|
|
249
|
+
if tool_use_data:
|
|
250
|
+
simplified["tool_use"] = tool_use_data
|
|
251
|
+
else:
|
|
252
|
+
simplified["content"] = text_content
|
|
253
|
+
|
|
254
|
+
return json.dumps(simplified)
|
|
255
|
+
else:
|
|
256
|
+
# For other message types, show full message with datetime and counter
|
|
257
|
+
# Type field is already present in data, so it's preserved
|
|
258
|
+
output = {
|
|
259
|
+
"datetime": now,
|
|
260
|
+
"counter": f"#{self.message_counter}",
|
|
261
|
+
**data
|
|
262
|
+
}
|
|
263
|
+
return json.dumps(output)
|
|
264
|
+
|
|
265
|
+
except json.JSONDecodeError:
|
|
266
|
+
# If not valid JSON, return as-is
|
|
267
|
+
return json_line
|
|
268
|
+
except Exception as e:
|
|
269
|
+
# On any error, return original line
|
|
270
|
+
print(f"Warning: Error formatting JSON: {e}", file=sys.stderr)
|
|
271
|
+
return json_line
|
|
272
|
+
|
|
273
|
+
def run_claude(self, cmd: List[str], verbose: bool = False, pretty: bool = True) -> int:
|
|
274
|
+
"""Execute the claude command and stream output"""
|
|
275
|
+
if verbose:
|
|
276
|
+
print(f"Executing: {' '.join(cmd)}", file=sys.stderr)
|
|
277
|
+
print("-" * 80, file=sys.stderr)
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
# Change to project directory before running
|
|
281
|
+
original_cwd = os.getcwd()
|
|
282
|
+
os.chdir(self.project_path)
|
|
283
|
+
|
|
284
|
+
# Run the command and stream output
|
|
285
|
+
# Use line buffering (bufsize=1) to ensure each JSON line is output immediately
|
|
286
|
+
process = subprocess.Popen(
|
|
287
|
+
cmd,
|
|
288
|
+
stdout=subprocess.PIPE,
|
|
289
|
+
stderr=subprocess.PIPE,
|
|
290
|
+
text=True,
|
|
291
|
+
bufsize=1, # Line buffering for immediate output
|
|
292
|
+
universal_newlines=True
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Stream stdout line by line (each line is a JSON object when using stream-json)
|
|
296
|
+
# This allows users to pipe to jq and see output as it streams
|
|
297
|
+
if process.stdout:
|
|
298
|
+
for line in process.stdout:
|
|
299
|
+
# Apply pretty formatting if enabled
|
|
300
|
+
if pretty:
|
|
301
|
+
formatted_line = self.pretty_format_json(line.strip())
|
|
302
|
+
if formatted_line:
|
|
303
|
+
print(formatted_line, flush=True)
|
|
304
|
+
else:
|
|
305
|
+
# Raw output without formatting
|
|
306
|
+
print(line, end='', flush=True)
|
|
307
|
+
|
|
308
|
+
# Wait for process to complete
|
|
309
|
+
process.wait()
|
|
310
|
+
|
|
311
|
+
# Print stderr if there were errors
|
|
312
|
+
if process.stderr and process.returncode != 0:
|
|
313
|
+
stderr_output = process.stderr.read()
|
|
314
|
+
if stderr_output:
|
|
315
|
+
print(stderr_output, file=sys.stderr)
|
|
316
|
+
|
|
317
|
+
# Restore original working directory
|
|
318
|
+
os.chdir(original_cwd)
|
|
319
|
+
|
|
320
|
+
return process.returncode
|
|
321
|
+
|
|
322
|
+
except KeyboardInterrupt:
|
|
323
|
+
print("\nInterrupted by user", file=sys.stderr)
|
|
324
|
+
if process:
|
|
325
|
+
process.terminate()
|
|
326
|
+
process.wait()
|
|
327
|
+
# Restore original working directory
|
|
328
|
+
if 'original_cwd' in locals():
|
|
329
|
+
os.chdir(original_cwd)
|
|
330
|
+
return 130
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f"Error executing claude: {e}", file=sys.stderr)
|
|
333
|
+
# Restore original working directory
|
|
334
|
+
if 'original_cwd' in locals():
|
|
335
|
+
os.chdir(original_cwd)
|
|
336
|
+
return 1
|
|
337
|
+
|
|
338
|
+
def run(self) -> int:
|
|
339
|
+
"""Main execution flow"""
|
|
340
|
+
# Parse arguments first to handle --help
|
|
341
|
+
args = self.parse_arguments()
|
|
342
|
+
|
|
343
|
+
# Check if prompt is provided
|
|
344
|
+
if not args.prompt and not args.prompt_file:
|
|
345
|
+
print(
|
|
346
|
+
"Error: Either -p/--prompt or -pp/--prompt-file is required.",
|
|
347
|
+
file=sys.stderr
|
|
348
|
+
)
|
|
349
|
+
print("\nRun 'claude.py --help' for usage information.", file=sys.stderr)
|
|
350
|
+
return 1
|
|
351
|
+
|
|
352
|
+
# Check if claude is installed
|
|
353
|
+
if not self.check_claude_installed():
|
|
354
|
+
print(
|
|
355
|
+
"Error: Claude CLI is not available. Please install it.",
|
|
356
|
+
file=sys.stderr
|
|
357
|
+
)
|
|
358
|
+
print(
|
|
359
|
+
"Visit: https://docs.anthropic.com/en/docs/agents-and-tools/claude-code for installation instructions",
|
|
360
|
+
file=sys.stderr
|
|
361
|
+
)
|
|
362
|
+
return 1
|
|
363
|
+
|
|
364
|
+
# Set configuration from arguments
|
|
365
|
+
self.project_path = os.path.abspath(args.cd)
|
|
366
|
+
self.model_name = args.model
|
|
367
|
+
self.auto_instruction = args.auto_instruction
|
|
368
|
+
|
|
369
|
+
# Get prompt from file or argument
|
|
370
|
+
if args.prompt_file:
|
|
371
|
+
self.prompt = self.read_prompt_file(args.prompt_file)
|
|
372
|
+
else:
|
|
373
|
+
self.prompt = args.prompt
|
|
374
|
+
|
|
375
|
+
# Validate project path
|
|
376
|
+
if not os.path.isdir(self.project_path):
|
|
377
|
+
print(
|
|
378
|
+
f"Error: Project path does not exist: {self.project_path}",
|
|
379
|
+
file=sys.stderr
|
|
380
|
+
)
|
|
381
|
+
return 1
|
|
382
|
+
|
|
383
|
+
# Build and execute command
|
|
384
|
+
cmd = self.build_claude_command(args)
|
|
385
|
+
pretty = args.pretty == "true"
|
|
386
|
+
self.verbose = args.verbose
|
|
387
|
+
return self.run_claude(cmd, verbose=args.verbose, pretty=pretty)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def main():
|
|
391
|
+
"""Entry point"""
|
|
392
|
+
service = ClaudeService()
|
|
393
|
+
sys.exit(service.run())
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
if __name__ == "__main__":
|
|
397
|
+
main()
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Codex Service Script for juno-code
|
|
4
|
+
This script provides a wrapper around OpenAI Codex CLI with configurable options.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional, List, Dict, Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CodexService:
|
|
17
|
+
"""Service wrapper for OpenAI Codex CLI"""
|
|
18
|
+
|
|
19
|
+
# Default configuration
|
|
20
|
+
DEFAULT_MODEL = "gpt-4"
|
|
21
|
+
DEFAULT_AUTO_INSTRUCTION = """You are an AI coding assistant. Follow the instructions provided and generate high-quality code."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.model_name = self.DEFAULT_MODEL
|
|
25
|
+
self.auto_instruction = self.DEFAULT_AUTO_INSTRUCTION
|
|
26
|
+
self.project_path = os.getcwd()
|
|
27
|
+
self.prompt = ""
|
|
28
|
+
self.additional_args: List[str] = []
|
|
29
|
+
|
|
30
|
+
def check_codex_installed(self) -> bool:
|
|
31
|
+
"""Check if codex CLI is installed and available"""
|
|
32
|
+
try:
|
|
33
|
+
result = subprocess.run(
|
|
34
|
+
["which", "codex"],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
check=False
|
|
38
|
+
)
|
|
39
|
+
return result.returncode == 0
|
|
40
|
+
except Exception:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
def parse_arguments(self) -> argparse.Namespace:
|
|
44
|
+
"""Parse command line arguments"""
|
|
45
|
+
parser = argparse.ArgumentParser(
|
|
46
|
+
description="Codex Service - Wrapper for OpenAI Codex CLI",
|
|
47
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
48
|
+
epilog="""
|
|
49
|
+
Examples:
|
|
50
|
+
%(prog)s -p "Write a hello world function"
|
|
51
|
+
%(prog)s -pp prompt.txt --cd /path/to/project
|
|
52
|
+
%(prog)s -p "Add tests" -m gpt-4 -c custom_arg=value
|
|
53
|
+
"""
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Core arguments
|
|
57
|
+
prompt_group = parser.add_mutually_exclusive_group(required=False)
|
|
58
|
+
prompt_group.add_argument(
|
|
59
|
+
"-p", "--prompt",
|
|
60
|
+
type=str,
|
|
61
|
+
help="Prompt text to send to codex"
|
|
62
|
+
)
|
|
63
|
+
prompt_group.add_argument(
|
|
64
|
+
"-pp", "--prompt-file",
|
|
65
|
+
type=str,
|
|
66
|
+
help="Path to file containing the prompt"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--cd",
|
|
71
|
+
type=str,
|
|
72
|
+
default=os.getcwd(),
|
|
73
|
+
help="Project path (absolute path). Default: current directory"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"-m", "--model",
|
|
78
|
+
type=str,
|
|
79
|
+
default=self.DEFAULT_MODEL,
|
|
80
|
+
help=f"Model name. Default: {self.DEFAULT_MODEL}"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--auto-instruction",
|
|
85
|
+
type=str,
|
|
86
|
+
default=self.DEFAULT_AUTO_INSTRUCTION,
|
|
87
|
+
help="Auto instruction to prepend to prompt"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-c", "--config",
|
|
92
|
+
action="append",
|
|
93
|
+
dest="configs",
|
|
94
|
+
help="Additional codex config arguments (can be used multiple times)"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--json",
|
|
99
|
+
action="store_true",
|
|
100
|
+
help="Output in JSON format"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--verbose",
|
|
105
|
+
action="store_true",
|
|
106
|
+
help="Enable verbose output"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return parser.parse_args()
|
|
110
|
+
|
|
111
|
+
def read_prompt_file(self, file_path: str) -> str:
|
|
112
|
+
"""Read prompt from a file"""
|
|
113
|
+
try:
|
|
114
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
115
|
+
return f.read().strip()
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
print(f"Error: Prompt file not found: {file_path}", file=sys.stderr)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Error reading prompt file: {e}", file=sys.stderr)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
def build_codex_command(self, args: argparse.Namespace) -> List[str]:
|
|
124
|
+
"""Build the codex command with all arguments"""
|
|
125
|
+
# Start with base command
|
|
126
|
+
cmd = [
|
|
127
|
+
"codex",
|
|
128
|
+
"--cd", self.project_path,
|
|
129
|
+
"-m", self.model_name,
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
# Add default config arguments
|
|
133
|
+
default_configs = [
|
|
134
|
+
"include_apply_patch_tool=true",
|
|
135
|
+
"use_experimental_streamable_shell_tool=true",
|
|
136
|
+
"sandbox_mode=danger-full-access"
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
# Track which configs are already set
|
|
140
|
+
config_keys = set()
|
|
141
|
+
user_configs = []
|
|
142
|
+
|
|
143
|
+
# Process user-provided configs
|
|
144
|
+
if args.configs:
|
|
145
|
+
for config in args.configs:
|
|
146
|
+
key = config.split('=')[0] if '=' in config else config
|
|
147
|
+
config_keys.add(key)
|
|
148
|
+
user_configs.append(config)
|
|
149
|
+
|
|
150
|
+
# Add default configs that weren't overridden
|
|
151
|
+
for config in default_configs:
|
|
152
|
+
key = config.split('=')[0]
|
|
153
|
+
if key not in config_keys:
|
|
154
|
+
cmd.extend(["-c", config])
|
|
155
|
+
|
|
156
|
+
# Add user configs (these will override defaults if keys match)
|
|
157
|
+
for config in user_configs:
|
|
158
|
+
cmd.extend(["-c", config])
|
|
159
|
+
|
|
160
|
+
# Build the full prompt (auto_instruction + user prompt)
|
|
161
|
+
full_prompt = f"{self.auto_instruction}\n\n{self.prompt}"
|
|
162
|
+
|
|
163
|
+
# Add exec command with prompt
|
|
164
|
+
cmd.extend(["exec", full_prompt])
|
|
165
|
+
|
|
166
|
+
# Add JSON flag if requested
|
|
167
|
+
if args.json:
|
|
168
|
+
cmd.append("--json")
|
|
169
|
+
|
|
170
|
+
return cmd
|
|
171
|
+
|
|
172
|
+
def run_codex(self, cmd: List[str], verbose: bool = False) -> int:
|
|
173
|
+
"""Execute the codex command and stream output"""
|
|
174
|
+
if verbose:
|
|
175
|
+
print(f"Executing: {' '.join(cmd)}", file=sys.stderr)
|
|
176
|
+
print("-" * 80, file=sys.stderr)
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
# Run the command and stream output
|
|
180
|
+
process = subprocess.Popen(
|
|
181
|
+
cmd,
|
|
182
|
+
stdout=subprocess.PIPE,
|
|
183
|
+
stderr=subprocess.PIPE,
|
|
184
|
+
text=True,
|
|
185
|
+
bufsize=1,
|
|
186
|
+
universal_newlines=True
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Stream stdout
|
|
190
|
+
if process.stdout:
|
|
191
|
+
for line in process.stdout:
|
|
192
|
+
print(line, end='')
|
|
193
|
+
|
|
194
|
+
# Wait for process to complete
|
|
195
|
+
process.wait()
|
|
196
|
+
|
|
197
|
+
# Print stderr if there were errors
|
|
198
|
+
if process.stderr and process.returncode != 0:
|
|
199
|
+
stderr_output = process.stderr.read()
|
|
200
|
+
if stderr_output:
|
|
201
|
+
print(stderr_output, file=sys.stderr)
|
|
202
|
+
|
|
203
|
+
return process.returncode
|
|
204
|
+
|
|
205
|
+
except KeyboardInterrupt:
|
|
206
|
+
print("\nInterrupted by user", file=sys.stderr)
|
|
207
|
+
if process:
|
|
208
|
+
process.terminate()
|
|
209
|
+
process.wait()
|
|
210
|
+
return 130
|
|
211
|
+
except Exception as e:
|
|
212
|
+
print(f"Error executing codex: {e}", file=sys.stderr)
|
|
213
|
+
return 1
|
|
214
|
+
|
|
215
|
+
def run(self) -> int:
|
|
216
|
+
"""Main execution flow"""
|
|
217
|
+
# Parse arguments first to handle --help
|
|
218
|
+
args = self.parse_arguments()
|
|
219
|
+
|
|
220
|
+
# Check if prompt is provided
|
|
221
|
+
if not args.prompt and not args.prompt_file:
|
|
222
|
+
print(
|
|
223
|
+
"Error: Either -p/--prompt or -pp/--prompt-file is required.",
|
|
224
|
+
file=sys.stderr
|
|
225
|
+
)
|
|
226
|
+
print("\nRun 'codex.py --help' for usage information.", file=sys.stderr)
|
|
227
|
+
return 1
|
|
228
|
+
|
|
229
|
+
# Check if codex is installed
|
|
230
|
+
if not self.check_codex_installed():
|
|
231
|
+
print(
|
|
232
|
+
"Error: OpenAI Codex is not available. Please install it.",
|
|
233
|
+
file=sys.stderr
|
|
234
|
+
)
|
|
235
|
+
print(
|
|
236
|
+
"Visit: https://openai.com/blog/openai-codex for installation instructions",
|
|
237
|
+
file=sys.stderr
|
|
238
|
+
)
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
# Set configuration from arguments
|
|
242
|
+
self.project_path = os.path.abspath(args.cd)
|
|
243
|
+
self.model_name = args.model
|
|
244
|
+
self.auto_instruction = args.auto_instruction
|
|
245
|
+
|
|
246
|
+
# Get prompt from file or argument
|
|
247
|
+
if args.prompt_file:
|
|
248
|
+
self.prompt = self.read_prompt_file(args.prompt_file)
|
|
249
|
+
else:
|
|
250
|
+
self.prompt = args.prompt
|
|
251
|
+
|
|
252
|
+
# Validate project path
|
|
253
|
+
if not os.path.isdir(self.project_path):
|
|
254
|
+
print(
|
|
255
|
+
f"Error: Project path does not exist: {self.project_path}",
|
|
256
|
+
file=sys.stderr
|
|
257
|
+
)
|
|
258
|
+
return 1
|
|
259
|
+
|
|
260
|
+
# Build and execute command
|
|
261
|
+
cmd = self.build_codex_command(args)
|
|
262
|
+
return self.run_codex(cmd, verbose=args.verbose)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def main():
|
|
266
|
+
"""Entry point"""
|
|
267
|
+
service = CodexService()
|
|
268
|
+
sys.exit(service.run())
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
main()
|