1use crate::api::{fetch_api_spec, format_api_spec_as_help};
3use crate::cli::{Cli, TypeArg};
4use crate::prompt::generate_system_prompt;
5use crate::tool_config::{setup_config_migrator, ToolConfig, ToolType};
6use crate::utils::{
7 find_tool_binary, generate_unique_aliases, get_tool_config_dir, get_tool_help,
8 resolve_tool_name,
9};
10use clap::CommandFactory;
11use clap_complete::{generate, Shell};
12use std::fs;
13use std::io;
14use std::path::PathBuf;
15use std::process::Command;
16
17enum DetectedType {
19 Cli { bin_path: String },
20 Api { spec_url: String, base_url: String },
21 Expert,
22}
23
24fn detect_tool_type(
26 name: &str,
27 explicit_type: Option<TypeArg>,
28 spec: Option<String>,
29 base_url: Option<String>,
30) -> anyhow::Result<(DetectedType, String)> {
31 if let Some(type_arg) = explicit_type {
33 match type_arg {
34 TypeArg::Api => {
35 let spec_url = spec
36 .ok_or_else(|| anyhow::anyhow!("--type api requires --spec and --base-url"))?;
37 let base_url_str = base_url
38 .ok_or_else(|| anyhow::anyhow!("--type api requires --spec and --base-url"))?;
39 println!("π§ Type: API (explicit --type)");
40 let api_spec = fetch_api_spec(&spec_url)?;
41 let help = format_api_spec_as_help(&api_spec);
42 return Ok((
43 DetectedType::Api {
44 spec_url,
45 base_url: base_url_str,
46 },
47 help,
48 ));
49 }
50 TypeArg::Cli => {
51 println!("π§ Type: CLI (explicit --type)");
52 let bin_path = find_tool_binary(name)?;
53 let help = get_tool_help(name)?;
54 return Ok((DetectedType::Cli { bin_path }, help));
55 }
56 TypeArg::Expert => {
57 println!("π§ Type: Expert (explicit --type)");
58 return Ok((DetectedType::Expert, String::new()));
59 }
60 }
61 }
62
63 if let (Some(spec_url), Some(base_url_str)) = (spec, base_url) {
65 println!("π Detected: API (--spec provided)");
66 let api_spec = fetch_api_spec(&spec_url)?;
67 let help = format_api_spec_as_help(&api_spec);
68 return Ok((
69 DetectedType::Api {
70 spec_url,
71 base_url: base_url_str,
72 },
73 help,
74 ));
75 }
76
77 if name.contains(' ') {
79 println!("π Detected: Expert (multi-word name)");
80 return Ok((DetectedType::Expert, String::new()));
81 }
82
83 println!("π Detecting type for '{}'...", name);
85 match find_tool_binary(name) {
86 Ok(bin_path) => {
87 match get_tool_help(name) {
88 Ok(help) => {
89 println!("β Found CLI tool at: {}", bin_path);
90 Ok((DetectedType::Cli { bin_path }, help))
91 }
92 Err(_) => {
93 println!("β Found CLI tool at: {} (no help available)", bin_path);
95 Ok((DetectedType::Cli { bin_path }, String::new()))
96 }
97 }
98 }
99 Err(_) => {
100 println!("β '{}' not found as CLI tool", name);
101 println!("β Registering as Expert");
102 Ok((DetectedType::Expert, String::new()))
103 }
104 }
105}
106
107pub fn handle_make(
109 tool_name: String,
110 persona: Option<String>,
111 explicit_type: Option<TypeArg>,
112 spec: Option<String>,
113 base_url: Option<String>,
114) -> anyhow::Result<()> {
115 println!("β¨ Creating configuration for: {}", tool_name);
116 println!();
117
118 let (detected, help_output) = detect_tool_type(&tool_name, explicit_type, spec, base_url)?;
120
121 let (tool_type, type_label) = match detected {
122 DetectedType::Cli { bin_path } => (ToolType::Cli { bin_path }, "CLI"),
123 DetectedType::Api { spec_url, base_url } => (ToolType::Api { spec_url, base_url }, "API"),
124 DetectedType::Expert => (ToolType::Expert, "Expert"),
125 };
126
127 if !help_output.is_empty() {
128 println!("π Retrieved documentation ({} bytes)", help_output.len());
129 }
130
131 let output_dir = get_tool_config_dir(&tool_name)?;
133 fs::create_dir_all(&output_dir)?;
134
135 let aliases = generate_unique_aliases(&tool_name)?;
137
138 let config = ToolConfig {
140 tool_name: tool_name.clone(),
141 tool_type: tool_type.clone(),
142 persona: persona.clone(),
143 aliases: aliases.clone(),
144 created_at: chrono::Utc::now().to_rfc3339(),
145 };
146
147 let migrator = setup_config_migrator()?;
148 let config_json = migrator.save_domain("tool_config", config)?;
149
150 let config_path = output_dir.join("config.json");
151 fs::write(&config_path, config_json)?;
152
153 let help_path = output_dir.join("help.txt");
155 fs::write(&help_path, &help_output)?;
156
157 let system_prompt = generate_system_prompt(&tool_name, &persona, &help_output, &tool_type);
159 let prompt_path = output_dir.join("system_prompt.txt");
160 fs::write(&prompt_path, &system_prompt)?;
161
162 println!();
164 println!("ββββββββββββββββββββββββββββββββββββββββββ");
165 println!("β β
Registered: {} ({})", tool_name, type_label);
166 if let Some(ref p) = persona {
167 println!("β π Persona: {}", p);
168 }
169 match &tool_type {
170 ToolType::Cli { bin_path } => {
171 println!("β π Binary: {}", bin_path);
172 }
173 ToolType::Api { base_url, .. } => {
174 println!("β π Base URL: {}", base_url);
175 }
176 ToolType::Expert => {
177 println!("β π‘ Generic expertise persona");
178 }
179 }
180 if !aliases.is_empty() {
181 println!("β π·οΈ Aliases: {}", aliases.join(", "));
182 }
183 println!("β π {}", output_dir.display());
184 println!("ββββββββββββββββββββββββββββββββββββββββββ");
185 println!();
186 if !aliases.is_empty() {
187 println!("π‘ Run: casting repl {} (or \"{}\")", aliases[0], tool_name);
188 } else {
189 println!("π‘ Run: casting repl \"{}\"", tool_name);
190 }
191
192 Ok(())
193}
194
195pub fn handle_list() -> anyhow::Result<()> {
197 let home = std::env::var("HOME")?;
198 let tools_dir = PathBuf::from(home).join(".casting").join("tools");
199
200 if !tools_dir.exists() {
201 println!("π No tools configured yet.");
202 println!("\nπ‘ Create your first tool with: casting make <TOOL> --persona <KEYWORD>");
203 return Ok(());
204 }
205
206 let migrator = setup_config_migrator()?;
207 let entries = fs::read_dir(&tools_dir)?;
208 let mut configs = Vec::new();
209
210 for entry in entries {
211 let entry = entry?;
212 let path = entry.path();
213
214 if path.is_dir() {
215 let config_path = path.join("config.json");
216 if config_path.exists() {
217 let config_str = fs::read_to_string(&config_path)?;
218
219 let config: ToolConfig = migrator.load_with_fallback("tool_config", &config_str)?;
221 configs.push(config);
222 }
223 }
224 }
225
226 if configs.is_empty() {
227 println!("π No tools configured yet.");
228 println!("\nπ‘ Create your first tool with: casting make <TOOL> --persona <KEYWORD>");
229 return Ok(());
230 }
231
232 println!("π Configured Tools:\n");
233
234 for config in configs {
235 println!(" {}", config.tool_name);
236 if let Some(persona) = &config.persona {
237 println!(" π Persona: {}", persona);
238 } else {
239 println!(" π Persona: (none)");
240 }
241
242 match &config.tool_type {
243 ToolType::Cli { bin_path } => {
244 println!(" π» Type: CLI");
245 println!(" π Binary: {}", bin_path);
246 }
247 ToolType::Api { spec_url, base_url } => {
248 println!(" π Type: API");
249 println!(" π Base URL: {}", base_url);
250 println!(" π Spec: {}", spec_url);
251 }
252 ToolType::Expert => {
253 println!(" π§ Type: Expert");
254 }
255 }
256
257 if !config.aliases.is_empty() {
258 println!(" π·οΈ Aliases: {}", config.aliases.join(", "));
259 }
260
261 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&config.created_at) {
263 println!(" π
Created: {}", dt.format("%Y-%m-%d %H:%M:%S"));
264 } else {
265 println!(" π
Created: {}", config.created_at);
266 }
267 println!();
268 }
269
270 println!("π‘ Use with: casting repl <TOOL> (or alias)");
271
272 Ok(())
273}
274
275pub fn handle_repl(tool: Option<String>) -> anyhow::Result<()> {
277 let mut cmd = Command::new("claude");
278
279 if let Some(ref input_name) = tool {
280 let tool_name = resolve_tool_name(input_name)?.unwrap_or_else(|| input_name.clone());
282
283 if &tool_name != input_name {
284 println!("π Resolved alias '{}' β '{}'", input_name, tool_name);
285 }
286 println!("π¬ Starting REPL for: {}", tool_name);
287
288 let prompt_path = get_tool_config_dir(&tool_name)?.join("system_prompt.txt");
290
291 if !prompt_path.exists() {
292 anyhow::bail!(
293 "Tool '{}' is not configured. Run: casting make \"{}\"",
294 input_name,
295 input_name
296 );
297 }
298
299 let system_prompt = fs::read_to_string(&prompt_path)?;
300 println!("π Loaded system prompt from: {}", prompt_path.display());
301
302 cmd.arg("--system-prompt").arg(&system_prompt);
304 } else {
305 println!("π¬ Starting Claude Code REPL...");
306 }
307
308 let status = cmd.status()?;
310
311 if !status.success() {
312 anyhow::bail!("Claude Code exited with error");
313 }
314
315 Ok(())
316}
317
318pub fn handle_completion(shell: Shell) -> anyhow::Result<()> {
320 let mut cmd = Cli::command();
321 let name = cmd.get_name().to_string();
322 generate(shell, &mut cmd, name, &mut io::stdout());
323 Ok(())
324}