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 Kaiba { profile: String },
23}
24
25fn detect_tool_type(
27 name: &str,
28 explicit_type: Option<TypeArg>,
29 spec: Option<String>,
30 base_url: Option<String>,
31 profile: Option<String>,
32) -> anyhow::Result<(DetectedType, String)> {
33 if let Some(type_arg) = explicit_type {
35 match type_arg {
36 TypeArg::Api => {
37 let spec_url = spec
38 .ok_or_else(|| anyhow::anyhow!("--type api requires --spec and --base-url"))?;
39 let base_url_str = base_url
40 .ok_or_else(|| anyhow::anyhow!("--type api requires --spec and --base-url"))?;
41 println!("π§ Type: API (explicit --type)");
42 let api_spec = fetch_api_spec(&spec_url)?;
43 let help = format_api_spec_as_help(&api_spec);
44 return Ok((
45 DetectedType::Api {
46 spec_url,
47 base_url: base_url_str,
48 },
49 help,
50 ));
51 }
52 TypeArg::Cli => {
53 println!("π§ Type: CLI (explicit --type)");
54 let bin_path = find_tool_binary(name)?;
55 let help = get_tool_help(name)?;
56 return Ok((DetectedType::Cli { bin_path }, help));
57 }
58 TypeArg::Expert => {
59 println!("π§ Type: Expert (explicit --type)");
60 return Ok((DetectedType::Expert, String::new()));
61 }
62 TypeArg::Kaiba => {
63 let profile_name = profile
64 .ok_or_else(|| anyhow::anyhow!("--type kaiba requires --profile <NAME>"))?;
65 println!("π§ Type: Kaiba (explicit --type)");
66 println!("π§ Profile: {}", profile_name);
67 if Command::new("kaiba").arg("--version").output().is_err() {
69 anyhow::bail!(
70 "kaiba CLI not found. Install with: cargo install --path crates/kaiba-cli"
71 );
72 }
73 return Ok((
74 DetectedType::Kaiba {
75 profile: profile_name,
76 },
77 String::new(),
78 ));
79 }
80 }
81 }
82
83 if let (Some(spec_url), Some(base_url_str)) = (spec, base_url) {
85 println!("π Detected: API (--spec provided)");
86 let api_spec = fetch_api_spec(&spec_url)?;
87 let help = format_api_spec_as_help(&api_spec);
88 return Ok((
89 DetectedType::Api {
90 spec_url,
91 base_url: base_url_str,
92 },
93 help,
94 ));
95 }
96
97 if name.contains(' ') {
99 println!("π Detected: Expert (multi-word name)");
100 return Ok((DetectedType::Expert, String::new()));
101 }
102
103 println!("π Detecting type for '{}'...", name);
105 match find_tool_binary(name) {
106 Ok(bin_path) => {
107 match get_tool_help(name) {
108 Ok(help) => {
109 println!("β Found CLI tool at: {}", bin_path);
110 Ok((DetectedType::Cli { bin_path }, help))
111 }
112 Err(_) => {
113 println!("β Found CLI tool at: {} (no help available)", bin_path);
115 Ok((DetectedType::Cli { bin_path }, String::new()))
116 }
117 }
118 }
119 Err(_) => {
120 println!("β '{}' not found as CLI tool", name);
121 println!("β Registering as Expert");
122 Ok((DetectedType::Expert, String::new()))
123 }
124 }
125}
126
127pub fn handle_make(
129 tool_name: String,
130 persona: Option<String>,
131 explicit_type: Option<TypeArg>,
132 spec: Option<String>,
133 base_url: Option<String>,
134 profile: Option<String>,
135) -> anyhow::Result<()> {
136 println!("β¨ Creating configuration for: {}", tool_name);
137 println!();
138
139 let (detected, help_output) =
141 detect_tool_type(&tool_name, explicit_type, spec, base_url, profile)?;
142
143 let (tool_type, type_label) = match detected {
144 DetectedType::Cli { bin_path } => (ToolType::Cli { bin_path }, "CLI"),
145 DetectedType::Api { spec_url, base_url } => (ToolType::Api { spec_url, base_url }, "API"),
146 DetectedType::Expert => (ToolType::Expert, "Expert"),
147 DetectedType::Kaiba { profile } => (ToolType::Kaiba { profile }, "Kaiba"),
148 };
149
150 if !help_output.is_empty() {
151 println!("π Retrieved documentation ({} bytes)", help_output.len());
152 }
153
154 let output_dir = get_tool_config_dir(&tool_name)?;
156 fs::create_dir_all(&output_dir)?;
157
158 let aliases = generate_unique_aliases(&tool_name)?;
160
161 let config = ToolConfig {
163 tool_name: tool_name.clone(),
164 tool_type: tool_type.clone(),
165 persona: persona.clone(),
166 aliases: aliases.clone(),
167 created_at: chrono::Utc::now().to_rfc3339(),
168 };
169
170 let migrator = setup_config_migrator()?;
171 let config_json = migrator.save_domain("tool_config", config)?;
172
173 let config_path = output_dir.join("config.json");
174 fs::write(&config_path, config_json)?;
175
176 let help_path = output_dir.join("help.txt");
178 fs::write(&help_path, &help_output)?;
179
180 let system_prompt = generate_system_prompt(&tool_name, &persona, &help_output, &tool_type);
182 let prompt_path = output_dir.join("system_prompt.txt");
183 fs::write(&prompt_path, &system_prompt)?;
184
185 println!();
187 println!("ββββββββββββββββββββββββββββββββββββββββββ");
188 println!("β β
Registered: {} ({})", tool_name, type_label);
189 if let Some(ref p) = persona {
190 println!("β π Persona: {}", p);
191 }
192 match &tool_type {
193 ToolType::Cli { bin_path } => {
194 println!("β π Binary: {}", bin_path);
195 }
196 ToolType::Api { base_url, .. } => {
197 println!("β π Base URL: {}", base_url);
198 }
199 ToolType::Expert => {
200 println!("β π‘ Generic expertise persona");
201 }
202 ToolType::Kaiba { profile } => {
203 println!("β π§ Kaiba Profile: {}", profile);
204 println!("β π‘ Dynamic prompt from Kaiba API");
205 }
206 }
207 if !aliases.is_empty() {
208 println!("β π·οΈ Aliases: {}", aliases.join(", "));
209 }
210 println!("β π {}", output_dir.display());
211 println!("ββββββββββββββββββββββββββββββββββββββββββ");
212 println!();
213 if !aliases.is_empty() {
214 println!("π‘ Run: casting repl {} (or \"{}\")", aliases[0], tool_name);
215 } else {
216 println!("π‘ Run: casting repl \"{}\"", tool_name);
217 }
218
219 Ok(())
220}
221
222pub fn handle_list() -> anyhow::Result<()> {
224 let home = std::env::var("HOME")?;
225 let tools_dir = PathBuf::from(home).join(".casting").join("tools");
226
227 if !tools_dir.exists() {
228 println!("π No tools configured yet.");
229 println!("\nπ‘ Create your first tool with: casting make <TOOL> --persona <KEYWORD>");
230 return Ok(());
231 }
232
233 let migrator = setup_config_migrator()?;
234 let entries = fs::read_dir(&tools_dir)?;
235 let mut configs = Vec::new();
236
237 for entry in entries {
238 let entry = entry?;
239 let path = entry.path();
240
241 if path.is_dir() {
242 let config_path = path.join("config.json");
243 if config_path.exists() {
244 let config_str = fs::read_to_string(&config_path)?;
245
246 let config: ToolConfig = migrator.load_with_fallback("tool_config", &config_str)?;
248 configs.push(config);
249 }
250 }
251 }
252
253 if configs.is_empty() {
254 println!("π No tools configured yet.");
255 println!("\nπ‘ Create your first tool with: casting make <TOOL> --persona <KEYWORD>");
256 return Ok(());
257 }
258
259 println!("π Configured Tools:\n");
260
261 for config in configs {
262 println!(" {}", config.tool_name);
263 if let Some(persona) = &config.persona {
264 println!(" π Persona: {}", persona);
265 } else {
266 println!(" π Persona: (none)");
267 }
268
269 match &config.tool_type {
270 ToolType::Cli { bin_path } => {
271 println!(" π» Type: CLI");
272 println!(" π Binary: {}", bin_path);
273 }
274 ToolType::Api { spec_url, base_url } => {
275 println!(" π Type: API");
276 println!(" π Base URL: {}", base_url);
277 println!(" π Spec: {}", spec_url);
278 }
279 ToolType::Expert => {
280 println!(" π§ Type: Expert");
281 }
282 ToolType::Kaiba { profile } => {
283 println!(" π§ Type: Kaiba");
284 println!(" π Profile: {}", profile);
285 }
286 }
287
288 if !config.aliases.is_empty() {
289 println!(" π·οΈ Aliases: {}", config.aliases.join(", "));
290 }
291
292 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&config.created_at) {
294 println!(" π
Created: {}", dt.format("%Y-%m-%d %H:%M:%S"));
295 } else {
296 println!(" π
Created: {}", config.created_at);
297 }
298 println!();
299 }
300
301 println!("π‘ Use with: casting repl <TOOL> (or alias)");
302
303 Ok(())
304}
305
306pub fn handle_repl(tool: Option<String>) -> anyhow::Result<()> {
308 let mut cmd = Command::new("claude");
309
310 if let Some(ref input_name) = tool {
311 let tool_name = resolve_tool_name(input_name)?.unwrap_or_else(|| input_name.clone());
313
314 if &tool_name != input_name {
315 println!("π Resolved alias '{}' β '{}'", input_name, tool_name);
316 }
317 println!("π¬ Starting REPL for: {}", tool_name);
318
319 let config_path = get_tool_config_dir(&tool_name)?.join("config.json");
321
322 if !config_path.exists() {
323 anyhow::bail!(
324 "Tool '{}' is not configured. Run: casting make \"{}\"",
325 input_name,
326 input_name
327 );
328 }
329
330 let config_str = fs::read_to_string(&config_path)?;
331 let migrator = setup_config_migrator()?;
332 let config: ToolConfig = migrator.load_with_fallback("tool_config", &config_str)?;
333
334 let system_prompt = match &config.tool_type {
336 ToolType::Kaiba { profile } => {
337 println!(
339 "π§ Fetching dynamic prompt from Kaiba (profile: {})...",
340 profile
341 );
342
343 let output = Command::new("kaiba")
344 .args(["prompt", "-f", "casting", "-p", profile, "-m"])
345 .output()
346 .map_err(|e| {
347 anyhow::anyhow!("Failed to run kaiba CLI: {}. Is it installed?", e)
348 })?;
349
350 if !output.status.success() {
351 let stderr = String::from_utf8_lossy(&output.stderr);
352 anyhow::bail!("kaiba prompt failed: {}", stderr);
353 }
354
355 let prompt = String::from_utf8(output.stdout)
356 .map_err(|e| anyhow::anyhow!("Invalid UTF-8 from kaiba: {}", e))?;
357
358 println!("β Loaded dynamic prompt ({} bytes)", prompt.len());
359 prompt
360 }
361 _ => {
362 let prompt_path = get_tool_config_dir(&tool_name)?.join("system_prompt.txt");
364 let prompt = fs::read_to_string(&prompt_path)?;
365 println!("π Loaded system prompt from: {}", prompt_path.display());
366 prompt
367 }
368 };
369
370 cmd.arg("--system-prompt").arg(&system_prompt);
372 } else {
373 println!("π¬ Starting Claude Code REPL...");
374 }
375
376 let status = cmd.status()?;
378
379 if !status.success() {
380 anyhow::bail!("Claude Code exited with error");
381 }
382
383 Ok(())
384}
385
386pub fn handle_completion(shell: Shell) -> anyhow::Result<()> {
388 let mut cmd = Cli::command();
389 let name = cmd.get_name().to_string();
390 generate(shell, &mut cmd, name, &mut io::stdout());
391 Ok(())
392}