Skip to content

Commit 36acf70

Browse files
author
Openbot
committed
fix: use clap Subcommand for CLI and std::io::IsTerminal instead of atty
- Replace manual args().nth(1) check with clap::Subcommand (Commands enum) - Add --output flag to openab setup command - Replace atty crate with std::io::IsTerminal (removes atty dependency)
1 parent 0b26935 commit 36acf70

4 files changed

Lines changed: 103 additions & 128 deletions

File tree

Cargo.lock

Lines changed: 0 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ anyhow = "1"
1717
rand = "0.8"
1818
clap = { version = "4", features = ["derive"] }
1919
rpassword = "7"
20-
atty = "0.2"
2120
unicode-width = "0.1"
2221
ureq = { version = "2", features = ["json"] }
2322
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "multipart", "json"] }

src/main.rs

Lines changed: 101 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,30 @@ mod reactions;
77
mod setup;
88
mod stt;
99

10+
use clap::Parser;
1011
use serenity::prelude::*;
1112
use std::collections::HashSet;
1213
use std::path::PathBuf;
1314
use std::sync::Arc;
1415
use tracing::info;
1516

17+
#[derive(Parser)]
18+
#[command(name = "openab")]
19+
#[command(about = "Discord bot that manages ACP agent sessions", long_about = None)]
20+
enum Commands {
21+
/// Run the bot (default)
22+
Run {
23+
/// Config file path (default: config.toml)
24+
config: Option<String>,
25+
},
26+
/// Launch the interactive setup wizard
27+
Setup {
28+
/// Output file path for generated config (default: config.toml)
29+
#[arg(short, long)]
30+
output: Option<String>,
31+
},
32+
}
33+
1634
#[tokio::main]
1735
async fn main() -> anyhow::Result<()> {
1836
tracing_subscriber::fmt()
@@ -22,94 +40,95 @@ async fn main() -> anyhow::Result<()> {
2240
)
2341
.init();
2442

25-
// Setup wizard mode
26-
if let Some(arg) = std::env::args().nth(1) {
27-
if arg == "setup" {
28-
setup::run_setup(None)?;
43+
let cmd = Commands::parse();
44+
45+
match cmd {
46+
Commands::Setup { output } => {
47+
setup::run_setup(output.map(PathBuf::from))?;
2948
return Ok(());
3049
}
31-
}
32-
33-
let config_path = std::env::args()
34-
.nth(1)
35-
.map(PathBuf::from)
36-
.unwrap_or_else(|| PathBuf::from("config.toml"));
37-
38-
let mut cfg = config::load_config(&config_path)?;
39-
info!(
40-
agent_cmd = %cfg.agent.command,
41-
pool_max = cfg.pool.max_sessions,
42-
channels = ?cfg.discord.allowed_channels,
43-
users = ?cfg.discord.allowed_users,
44-
reactions = cfg.reactions.enabled,
45-
"config loaded"
46-
);
47-
48-
let pool = Arc::new(acp::SessionPool::new(cfg.agent, cfg.pool.max_sessions));
49-
let ttl_secs = cfg.pool.session_ttl_hours * 3600;
50-
51-
let allowed_channels = parse_id_set(&cfg.discord.allowed_channels, "allowed_channels")?;
52-
let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?;
53-
info!(channels = allowed_channels.len(), users = allowed_users.len(), "parsed allowlists");
54-
55-
// Resolve STT config before constructing handler (auto-detect mutates cfg.stt)
56-
if cfg.stt.enabled {
57-
if cfg.stt.api_key.is_empty() && cfg.stt.base_url.contains("groq.com") {
58-
if let Ok(key) = std::env::var("GROQ_API_KEY") {
59-
if !key.is_empty() {
60-
info!("stt.api_key not set, using GROQ_API_KEY from environment");
61-
cfg.stt.api_key = key;
50+
Commands::Run { config } => {
51+
let config_path = config
52+
.map(PathBuf::from)
53+
.unwrap_or_else(|| PathBuf::from("config.toml"));
54+
55+
let mut cfg = config::load_config(&config_path)?;
56+
info!(
57+
agent_cmd = %cfg.agent.command,
58+
pool_max = cfg.pool.max_sessions,
59+
channels = ?cfg.discord.allowed_channels,
60+
users = ?cfg.discord.allowed_users,
61+
reactions = cfg.reactions.enabled,
62+
"config loaded"
63+
);
64+
65+
let pool = Arc::new(acp::SessionPool::new(cfg.agent, cfg.pool.max_sessions));
66+
let ttl_secs = cfg.pool.session_ttl_hours * 3600;
67+
68+
let allowed_channels = parse_id_set(&cfg.discord.allowed_channels, "allowed_channels")?;
69+
let allowed_users = parse_id_set(&cfg.discord.allowed_users, "allowed_users")?;
70+
info!(channels = allowed_channels.len(), users = allowed_users.len(), "parsed allowlists");
71+
72+
// Resolve STT config before constructing handler (auto-detect mutates cfg.stt)
73+
if cfg.stt.enabled {
74+
if cfg.stt.api_key.is_empty() && cfg.stt.base_url.contains("groq.com") {
75+
if let Ok(key) = std::env::var("GROQ_API_KEY") {
76+
if !key.is_empty() {
77+
info!("stt.api_key not set, using GROQ_API_KEY from environment");
78+
cfg.stt.api_key = key;
79+
}
80+
}
81+
}
82+
if cfg.stt.api_key.is_empty() {
83+
anyhow::bail!("stt.enabled = true but no API key found — set stt.api_key in config or export GROQ_API_KEY");
6284
}
85+
info!(model = %cfg.stt.model, base_url = %cfg.stt.base_url, "STT enabled");
6386
}
64-
}
65-
if cfg.stt.api_key.is_empty() {
66-
anyhow::bail!("stt.enabled = true but no API key found — set stt.api_key in config or export GROQ_API_KEY");
67-
}
68-
info!(model = %cfg.stt.model, base_url = %cfg.stt.base_url, "STT enabled");
69-
}
7087

71-
let handler = discord::Handler {
72-
pool: pool.clone(),
73-
allowed_channels,
74-
allowed_users,
75-
reactions_config: cfg.reactions,
76-
stt_config: cfg.stt.clone(),
77-
};
78-
79-
let intents = GatewayIntents::GUILD_MESSAGES
80-
| GatewayIntents::MESSAGE_CONTENT
81-
| GatewayIntents::GUILDS;
82-
83-
let mut client = Client::builder(&cfg.discord.bot_token, intents)
84-
.event_handler(handler)
85-
.await?;
86-
87-
// Spawn cleanup task
88-
let cleanup_pool = pool.clone();
89-
let cleanup_handle = tokio::spawn(async move {
90-
loop {
91-
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
92-
cleanup_pool.cleanup_idle(ttl_secs).await;
88+
let handler = discord::Handler {
89+
pool: pool.clone(),
90+
allowed_channels,
91+
allowed_users,
92+
reactions_config: cfg.reactions,
93+
stt_config: cfg.stt.clone(),
94+
};
95+
96+
let intents = GatewayIntents::GUILD_MESSAGES
97+
| GatewayIntents::MESSAGE_CONTENT
98+
| GatewayIntents::GUILDS;
99+
100+
let mut client = Client::builder(&cfg.discord.bot_token, intents)
101+
.event_handler(handler)
102+
.await?;
103+
104+
// Spawn cleanup task
105+
let cleanup_pool = pool.clone();
106+
let cleanup_handle = tokio::spawn(async move {
107+
loop {
108+
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
109+
cleanup_pool.cleanup_idle(ttl_secs).await;
110+
}
111+
});
112+
113+
// Run bot until SIGINT/SIGTERM
114+
let shard_manager = client.shard_manager.clone();
115+
let shutdown_pool = pool.clone();
116+
tokio::spawn(async move {
117+
tokio::signal::ctrl_c().await.ok();
118+
info!("shutdown signal received");
119+
shard_manager.shutdown_all().await;
120+
});
121+
122+
info!("starting discord bot");
123+
client.start().await?;
124+
125+
// Cleanup
126+
cleanup_handle.abort();
127+
shutdown_pool.shutdown().await;
128+
info!("openab shut down");
129+
Ok(())
93130
}
94-
});
95-
96-
// Run bot until SIGINT/SIGTERM
97-
let shard_manager = client.shard_manager.clone();
98-
let shutdown_pool = pool.clone();
99-
tokio::spawn(async move {
100-
tokio::signal::ctrl_c().await.ok();
101-
info!("shutdown signal received");
102-
shard_manager.shutdown_all().await;
103-
});
104-
105-
info!("starting discord bot");
106-
client.start().await?;
107-
108-
// Cleanup
109-
cleanup_handle.abort();
110-
shutdown_pool.shutdown().await;
111-
info!("openab shut down");
112-
Ok(())
131+
}
113132
}
114133

115134
fn parse_id_set(raw: &[String], label: &str) -> anyhow::Result<HashSet<u64>> {

src/setup.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! Interactive setup wizard for OpenAB.
22
3-
use std::io::{self, Write};
3+
use std::io::{self, IsTerminal, Write};
44
use std::path::{Path, PathBuf};
55

66
// ---------------------------------------------------------------------------
@@ -41,7 +41,7 @@ macro_rules! cprintln {
4141
// ---------------------------------------------------------------------------
4242

4343
fn is_interactive() -> bool {
44-
atty::is(atty::Stream::Stdout) && atty::is(atty::Stream::Stdin)
44+
std::io::stdin().is_terminal() && std::io::stdout().is_terminal()
4545
}
4646

4747
fn prompt(prompt_text: &str) -> String {

0 commit comments

Comments
 (0)