juno-code 1.0.14 → 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.
@@ -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()