Coverage for src / moai_adk / core / claude_integration.py: 13.91%
115 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""
2Claude Code CLI integration for advanced variable substitution and automation.
4Enables headless operation with template variable processing, JSON streaming,
5and multi-language support for commands, agents, and output styles.
6"""
8import json
9import subprocess
10import tempfile
11from pathlib import Path
12from typing import Any, Dict, Optional, Union
14from .language_config import get_language_info
15from .template_engine import TemplateEngine
18class ClaudeCLIIntegration:
19 """
20 Advanced Claude CLI integration with template variable processing.
22 Features:
23 - Template variable substitution using MoAI-ADK's TemplateEngine
24 - JSON streaming input/output support
25 - Multi-language description processing
26 - Headless automation capabilities
27 - Configuration file generation and management
28 """
30 def __init__(self, template_engine: Optional[TemplateEngine] = None):
31 """Initialize Claude CLI integration.
33 Args:
34 template_engine: TemplateEngine instance for variable processing
35 """
36 self.template_engine = template_engine or TemplateEngine()
38 def generate_claude_settings(
39 self, variables: Dict[str, Any], output_path: Optional[Path] = None
40 ) -> Path:
41 """Generate Claude settings JSON file with variables.
43 Args:
44 variables: Template variables to include
45 output_path: Path for settings file (auto-generated if None)
47 Returns:
48 Path to generated settings file
49 """
50 if output_path is None:
51 temp_file = tempfile.NamedTemporaryFile(
52 mode="w", suffix=".json", delete=False
53 )
54 output_path = Path(temp_file.name)
55 temp_file.close()
57 settings = {
58 "variables": variables,
59 "template_context": {
60 "conversation_language": variables.get("CONVERSATION_LANGUAGE", "en"),
61 "conversation_language_name": variables.get(
62 "CONVERSATION_LANGUAGE_NAME", "English"
63 ),
64 "project_name": variables.get("PROJECT_NAME", ""),
65 "codebase_language": variables.get("CODEBASE_LANGUAGE", "python"),
66 },
67 }
69 output_path.write_text(json.dumps(settings, indent=2, ensure_ascii=False))
70 return output_path
72 def process_template_command(
73 self,
74 command_template: str,
75 variables: Dict[str, Any],
76 print_mode: bool = True,
77 output_format: str = "json",
78 ) -> Dict[str, Any]:
79 """Process Claude command with template variables.
81 Args:
82 command_template: Command template with {{VARIABLE}} placeholders
83 variables: Variables for substitution
84 print_mode: Use --print flag for non-interactive execution
85 output_format: Output format (text, json, stream-json)
87 Returns:
88 Process result dictionary
89 """
90 try:
91 # Process template variables
92 processed_command = self.template_engine.render_string(
93 command_template, variables
94 )
96 # Build Claude CLI command
97 cmd_parts = ["claude"]
99 if print_mode:
100 cmd_parts.extend(["--print"])
101 cmd_parts.extend(["--output-format", output_format])
103 # Add variable settings
104 settings_file = self.generate_claude_settings(variables)
105 cmd_parts.extend(["--settings", str(settings_file)])
107 # Add processed command
108 cmd_parts.append(processed_command)
110 # Execute Claude CLI
111 result = subprocess.run(
112 cmd_parts, capture_output=True, text=True, encoding="utf-8"
113 )
115 # Cleanup settings file
116 try:
117 settings_file.unlink()
118 except OSError:
119 pass # Ignore cleanup errors
121 return {
122 "success": result.returncode == 0,
123 "stdout": result.stdout,
124 "stderr": result.stderr,
125 "returncode": result.returncode,
126 "processed_command": processed_command,
127 "variables_used": variables,
128 }
130 except Exception as e:
131 return {
132 "success": False,
133 "error": str(e),
134 "processed_command": command_template,
135 "variables_used": variables,
136 }
138 def generate_multilingual_descriptions(
139 self,
140 base_descriptions: Dict[str, str],
141 target_languages: Optional[list[str]] = None,
142 ) -> Dict[str, Dict[str, str]]:
143 """Generate multilingual descriptions for commands/agents.
145 Args:
146 base_descriptions: English base descriptions
147 target_languages: Target language codes (auto-detected if None)
149 Returns:
150 Multilingual descriptions dictionary
151 """
152 if target_languages is None:
153 # Auto-detect from variables or use common languages
154 target_languages = ["en", "ko", "ja", "es", "fr", "de"]
156 multilingual = {}
158 for item_id, base_desc in base_descriptions.items():
159 multilingual[item_id] = {"en": base_desc}
161 # Generate descriptions for target languages
162 for lang_code in target_languages:
163 if lang_code == "en":
164 continue # Already have base
166 lang_info = get_language_info(lang_code)
167 if not lang_info:
168 continue
170 # Create language-specific description prompt
171 translation_prompt = f"""Translate the following Claude Code description to {lang_info['native_name']}.
172Keep technical terms in English. Provide only the translation without explanation:
174Original: {base_desc}
176Translation:"""
178 # Use Claude CLI for translation
179 translation_result = self.process_template_command(
180 translation_prompt,
181 {"CONVERSATION_LANGUAGE": lang_code},
182 print_mode=True,
183 output_format="text",
184 )
186 if translation_result["success"]:
187 # Extract translation from output
188 translation = translation_result["stdout"].strip()
189 if translation:
190 multilingual[item_id][lang_code] = translation
192 return multilingual
194 def create_agent_with_multilingual_support(
195 self,
196 agent_name: str,
197 base_description: str,
198 tools: list[str],
199 model: str = "sonnet",
200 target_languages: Optional[list[str]] = None,
201 ) -> Dict[str, Any]:
202 """Create Claude agent with multilingual description support.
204 Args:
205 agent_name: Agent name (kebab-case)
206 base_description: English base description
207 tools: List of required tools
208 model: Claude model to use
209 target_languages: Target languages for descriptions
211 Returns:
212 Agent configuration dictionary
213 """
214 if target_languages is None:
215 target_languages = ["en", "ko", "ja", "es", "fr", "de"]
217 # Generate multilingual descriptions
218 descriptions = self.generate_multilingual_descriptions(
219 {agent_name: base_description}, target_languages
220 )
222 agent_config = {
223 "name": agent_name,
224 "description": descriptions[agent_name][
225 "en"
226 ], # Primary English description
227 "tools": tools,
228 "model": model,
229 "descriptions": descriptions[agent_name], # All language versions
230 "multilingual_support": True,
231 }
233 return agent_config
235 def create_command_with_multilingual_support(
236 self,
237 command_name: str,
238 base_description: str,
239 argument_hint: list[str],
240 tools: list[str],
241 model: str = "haiku",
242 target_languages: Optional[list[str]] = None,
243 ) -> Dict[str, Any]:
244 """Create Claude command with multilingual description support.
246 Args:
247 command_name: Command name (kebab-case)
248 base_description: English base description
249 argument_hint: List of argument hints
250 tools: List of required tools
251 model: Claude model to use
252 target_languages: Target languages for descriptions
254 Returns:
255 Command configuration dictionary
256 """
257 if target_languages is None:
258 target_languages = ["en", "ko", "ja", "es", "fr", "de"]
260 # Generate multilingual descriptions
261 descriptions = self.generate_multilingual_descriptions(
262 {command_name: base_description}, target_languages
263 )
265 command_config = {
266 "name": command_name,
267 "description": descriptions[command_name][
268 "en"
269 ], # Primary English description
270 "argument-hint": argument_hint,
271 "tools": tools,
272 "model": model,
273 "descriptions": descriptions[command_name], # All language versions
274 "multilingual_support": True,
275 }
277 return command_config
279 def process_json_stream_input(
280 self,
281 input_data: Union[Dict[str, Any], str],
282 variables: Optional[Dict[str, Any]] = None,
283 ) -> Dict[str, Any]:
284 """Process JSON stream input for Claude CLI.
286 Args:
287 input_data: JSON data as dict or JSON string
288 variables: Additional variables for processing
290 Returns:
291 Processed input data
292 """
293 # Convert string to dict if needed
294 processed_data: Dict[str, Any]
295 if isinstance(input_data, str):
296 try:
297 processed_data = json.loads(input_data)
298 except json.JSONDecodeError as e:
299 raise ValueError(f"Invalid JSON input: {e}")
300 else:
301 processed_data = input_data
303 if variables:
304 # Process any string values in processed_data with variables
305 for key, value in processed_data.items():
306 if isinstance(value, str) and "{{" in value and "}}" in value:
307 processed_data[key] = self.template_engine.render_string(
308 value, variables
309 )
311 return processed_data
313 def execute_headless_command(
314 self,
315 prompt_template: str,
316 variables: Dict[str, Any],
317 input_format: str = "stream-json",
318 output_format: str = "stream-json",
319 additional_options: Optional[list[str]] = None,
320 ) -> Dict[str, Any]:
321 """Execute Claude command in headless mode with full variable processing.
323 Args:
324 prompt_template: Prompt template with variables
325 variables: Variables for substitution
326 input_format: Input format (text, stream-json)
327 output_format: Output format (text, json, stream-json)
328 additional_options: Additional CLI options
330 Returns:
331 Command execution result
332 """
333 try:
334 # Process prompt template
335 processed_prompt = self.template_engine.render_string(
336 prompt_template, variables
337 )
339 # Build Claude command
340 cmd_parts = ["claude", "--print"]
341 cmd_parts.extend(["--input-format", input_format])
342 cmd_parts.extend(["--output-format", output_format])
344 # Add settings
345 settings_file = self.generate_claude_settings(variables)
346 cmd_parts.extend(["--settings", str(settings_file)])
348 # Add additional options
349 if additional_options:
350 cmd_parts.extend(additional_options)
352 # Add processed prompt
353 cmd_parts.append(processed_prompt)
355 # Execute with streaming support
356 if output_format == "stream-json":
357 # For streaming, use subprocess with real-time output
358 process = subprocess.Popen(
359 cmd_parts,
360 stdout=subprocess.PIPE,
361 stderr=subprocess.PIPE,
362 text=True,
363 encoding="utf-8",
364 )
366 stdout_lines = []
367 stderr_lines = []
369 # Stream output in real-time
370 while True:
371 stdout_line = process.stdout.readline()
372 stderr_line = process.stderr.readline()
374 if (
375 stdout_line == ""
376 and stderr_line == ""
377 and process.poll() is not None
378 ):
379 break
381 if stdout_line:
382 stdout_lines.append(stdout_line.strip())
383 # You can process each line here for real-time handling
385 if stderr_line:
386 stderr_lines.append(stderr_line.strip())
388 returncode = process.poll()
390 else:
391 # Non-streaming execution
392 result = subprocess.run(
393 cmd_parts, capture_output=True, text=True, encoding="utf-8"
394 )
396 stdout_lines = (
397 result.stdout.strip().split("\n") if result.stdout else []
398 )
399 stderr_lines = (
400 result.stderr.strip().split("\n") if result.stderr else []
401 )
402 returncode = result.returncode
404 # Cleanup
405 try:
406 settings_file.unlink()
407 except OSError:
408 pass
410 return {
411 "success": returncode == 0,
412 "stdout": stdout_lines,
413 "stderr": stderr_lines,
414 "returncode": returncode,
415 "processed_prompt": processed_prompt,
416 "variables": variables,
417 }
419 except Exception as e:
420 return {
421 "success": False,
422 "error": str(e),
423 "prompt_template": prompt_template,
424 "variables": variables,
425 }