Skip to main content

dot_agent_core/
llm.rs

1//! LLM Integration Module
2//!
3//! Claude CLIを使用したLLM操作の共通機能を提供する。
4//!
5//! ## 使用方法
6//!
7//! ### Claude CLI可用性チェック
8//!
9//! ```rust
10//! use dot_agent_core::check_claude_cli;
11//!
12//! let available = check_claude_cli();
13//! println!("Claude CLI available: {}", available);
14//! ```
15//!
16//! ### LlmConfig
17//!
18//! ```rust
19//! use dot_agent_core::LlmConfig;
20//!
21//! let config = LlmConfig::default();
22//! assert!(!config.enabled);
23//! ```
24//!
25//! ### 完全な使用例(外部依存あり)
26//!
27//! ```rust,ignore
28//! use dot_agent_core::{check_claude_cli, execute_claude};
29//! use std::path::Path;
30//!
31//! if check_claude_cli() {
32//!     let working_dir = Path::new(".");
33//!     let result = execute_claude(working_dir, "Your prompt here")?;
34//!     println!("{}", result);
35//! }
36//! ```
37
38use std::io::Write as IoWrite;
39use std::path::Path;
40use std::process::{Command, Stdio};
41
42use serde::{Deserialize, Serialize};
43
44use crate::error::{DotAgentError, Result};
45
46// ============================================================================
47// Configuration
48// ============================================================================
49
50/// LLM機能の設定
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52pub struct LlmConfig {
53    /// LLM機能を有効にするか(デフォルト: false)
54    #[serde(default)]
55    pub enabled: bool,
56}
57
58// ============================================================================
59// CLI Operations
60// ============================================================================
61
62/// Claude CLIが利用可能かチェック
63///
64/// `claude --version` を実行して成功すればtrue
65pub fn check_claude_cli() -> bool {
66    Command::new("claude")
67        .arg("--version")
68        .stdout(Stdio::null())
69        .stderr(Stdio::null())
70        .status()
71        .map(|s| s.success())
72        .unwrap_or(false)
73}
74
75/// Claude CLIを実行してプロンプトを処理
76///
77/// # Arguments
78/// * `working_dir` - 作業ディレクトリ
79/// * `prompt` - 送信するプロンプト
80///
81/// # Returns
82/// Claude CLIの出力(stdout)
83///
84/// # Errors
85/// * `ClaudeNotFound` - Claude CLIが見つからない場合
86/// * `ClaudeExecutionFailed` - 実行に失敗した場合
87pub fn execute_claude(working_dir: &Path, prompt: &str) -> Result<String> {
88    let mut cmd = Command::new("claude");
89    cmd.arg("--print");
90    cmd.arg("--dangerously-skip-permissions");
91    cmd.current_dir(working_dir);
92    cmd.stdin(Stdio::piped());
93    cmd.stdout(Stdio::piped());
94    cmd.stderr(Stdio::piped());
95
96    let mut child = cmd
97        .spawn()
98        .map_err(|e| DotAgentError::ClaudeExecutionFailed {
99            message: format!("Failed to spawn claude: {}", e),
100        })?;
101
102    if let Some(mut stdin) = child.stdin.take() {
103        stdin
104            .write_all(prompt.as_bytes())
105            .map_err(|e| DotAgentError::ClaudeExecutionFailed {
106                message: format!("Failed to write prompt: {}", e),
107            })?;
108    }
109
110    let output = child
111        .wait_with_output()
112        .map_err(|e| DotAgentError::ClaudeExecutionFailed {
113            message: format!("Execution failed: {}", e),
114        })?;
115
116    if !output.status.success() {
117        let stderr = String::from_utf8_lossy(&output.stderr);
118        return Err(DotAgentError::ClaudeExecutionFailed {
119            message: format!("Claude exited with error: {}", stderr),
120        });
121    }
122
123    Ok(String::from_utf8_lossy(&output.stdout).to_string())
124}
125
126/// Claude CLIの存在を確認し、なければエラーを返す
127pub fn require_claude_cli() -> Result<()> {
128    if !check_claude_cli() {
129        return Err(DotAgentError::ClaudeNotFound);
130    }
131    Ok(())
132}
133
134// ============================================================================
135// Tests
136// ============================================================================
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_llm_config_default() {
144        let config = LlmConfig::default();
145        assert!(!config.enabled);
146    }
147
148    #[test]
149    fn test_llm_config_deserialize() {
150        let toml_str = r#"
151            enabled = true
152        "#;
153        let config: LlmConfig = toml::from_str(toml_str).unwrap();
154        assert!(config.enabled);
155    }
156
157    #[test]
158    fn test_llm_config_deserialize_empty() {
159        let toml_str = "";
160        let config: LlmConfig = toml::from_str(toml_str).unwrap();
161        assert!(!config.enabled);
162    }
163}