main
rs 102 lines 4.1 KB
Raw
1 //! splitfire-agent: a music-domain ACP agent (theory, production, industry, culture, live shows)
2 //! backed by Onde Inference, built on the Onde Agent Platform crates: `ed-acp` (the ACP server),
3 //! `ed-acp-tui` (the terminal UI) and `ed-mcp`. This crate holds only the music: its tools,
4 //! prompt, slash commands and the [`profile::SplitFire`] that wires them in.
5 //!
6 //! Run from a terminal it opens a TUI; launched by an editor (stdin not a TTY) or with `--acp`
7 //! it speaks ACP over stdio. Set ONDE_API_KEY (`app-id:app-secret`) or run `--setup`.
8 //! SPLITFIRE_YOLO=1 skips permission prompts. Logs go to stderr (RUST_LOG).
9
10 use std::io::IsTerminal;
11 use std::path::PathBuf;
12 use std::sync::Arc;
13
14 use ed_acp::{LlmConfig, ServeOptions, cli};
15 use ed_acp_tui::TuiConfig;
16
17 use splitfire_agent::{INFO, SplitFire};
18
19 const USAGE: &str = "\
20 Usage: splitfire-agent [--acp] [--yolo] [--list-models] [--root <path>...] [--setup] [--version]
21
22 (no args) interactive terminal UI (when run from a terminal)
23 --acp speak ACP over stdio for an editor (default when stdin is not a terminal)
24 --yolo approve file edits and commands without asking
25 --list-models list the models the Onde endpoint serves, then exit
26 --root <path> add an extra workspace root (repeatable; TUI mode only)
27 --setup interactive first-run setup: store your Onde API key (app-id:app-secret)
28 --version print the version
29
30 Environment: ONDE_API_KEY (required unless stored by --setup), SPLITFIRE_BASE_URL,
31 SPLITFIRE_MODEL, SPLITFIRE_MODELS, SPLITFIRE_YOLO, SPLITFIRE_DATA_DIR, SPLITFIRE_CONTEXT_WINDOW,
32 SPLITFIRE_DEMUCS_BIN, SPLITFIRE_MCP_DEMUCS=off, SPLITFIRE_STEMS_DIR
33 ";
34
35 #[tokio::main]
36 async fn main() -> anyhow::Result<()> {
37 let args: Vec<String> = std::env::args().skip(1).collect();
38 let has = |flag: &str| args.iter().any(|a| a == flag);
39 if has("-h") || has("--help") {
40 print!("{USAGE}");
41 return Ok(());
42 }
43 if has("-V") || has("--version") {
44 println!("{} {}", INFO.name, INFO.version);
45 return Ok(());
46 }
47 // Collect `--root <path>` pairs; validate everything else is a known flag.
48 let mut roots: Vec<PathBuf> = Vec::new();
49 let mut i = 0;
50 while i < args.len() {
51 match args[i].as_str() {
52 "--root" => {
53 i += 1;
54 let Some(path) = args.get(i) else {
55 anyhow::bail!("--root requires a path\n\n{USAGE}");
56 };
57 let path = PathBuf::from(path);
58 let path = if path.is_absolute() {
59 path
60 } else {
61 std::env::current_dir()?.join(path)
62 };
63 if !path.is_dir() {
64 anyhow::bail!("--root {} is not a directory", path.display());
65 }
66 roots.push(path);
67 }
68 "--acp" | "--yolo" | "--list-models" | "--setup" => {}
69 bad => anyhow::bail!("unknown argument {bad}\n\n{USAGE}"),
70 }
71 i += 1;
72 }
73 let mut opts = ServeOptions::from_env(&INFO);
74 opts.yolo |= has("--yolo");
75 if has("--setup") {
76 cli::setup(&INFO).await?;
77 } else if has("--list-models") {
78 cli::list_models(&INFO).await?;
79 } else if has("--acp") || !std::io::stdin().is_terminal() {
80 if !roots.is_empty() {
81 anyhow::bail!("--root is only supported in the interactive TUI\n\n{USAGE}");
82 }
83 cli::init_logging();
84 ed_acp::serve_stdio(Arc::new(SplitFire), opts).await?;
85 } else {
86 let llm = LlmConfig::from_env(&INFO.env);
87 if llm.api_key.is_none() {
88 // Exit 0: winget's validator launches the bare exe on a console and expects success.
89 eprintln!(
90 "No Onde API key configured. Set ONDE_API_KEY or run `{} --setup`.",
91 INFO.name
92 );
93 return Ok(());
94 }
95 let mut tui = TuiConfig::current_exe(INFO.name, opts.yolo)?;
96 tui.env.push((INFO.env.var("SURFACE"), "tui".to_string()));
97 tui.model_label = llm.model;
98 tui.extra_roots = roots;
99 ed_acp_tui::run(tui).await?;
100 }
101 Ok(())
102 }