Skip to content

Commit a027fad

Browse files
qhkmclaude
andcommitted
feat: add channel setup to express onboard and standalone channel setup commands
- Express onboard now offers Telegram/WhatsApp/Discord/Slack channel setup - `channel setup telegram` works standalone (no longer redirects to onboard) - `channel setup discord` works standalone with token + allowlist prompts - `channel setup slack` works standalone with bot + app token prompts - `channel setup webhook` works standalone with bind/port/auth prompts - Add configure_discord() and configure_slack() to onboard.rs for full wizard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b31648a commit a027fad

2 files changed

Lines changed: 258 additions & 16 deletions

File tree

src/cli/channel.rs

Lines changed: 161 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -137,22 +137,10 @@ async fn cmd_channel_setup(channel_name: &str) -> Result<()> {
137137

138138
match channel_name {
139139
"whatsapp_web" => setup_whatsapp_web(&mut config)?,
140-
"telegram" => {
141-
println!("Use 'zeptoclaw onboard' to configure Telegram.");
142-
return Ok(());
143-
}
144-
"discord" => {
145-
println!("Use 'zeptoclaw onboard' to configure Discord.");
146-
return Ok(());
147-
}
148-
"slack" => {
149-
println!("Use 'zeptoclaw onboard' to configure Slack.");
150-
return Ok(());
151-
}
152-
"webhook" => {
153-
println!("Use 'zeptoclaw onboard' to configure Webhook.");
154-
return Ok(());
155-
}
140+
"telegram" => setup_telegram(&mut config)?,
141+
"discord" => setup_discord(&mut config)?,
142+
"slack" => setup_slack(&mut config)?,
143+
"webhook" => setup_webhook(&mut config)?,
156144
_ => unreachable!(),
157145
}
158146

@@ -209,6 +197,163 @@ fn setup_whatsapp_web(config: &mut Config) -> Result<()> {
209197
Ok(())
210198
}
211199

200+
/// Interactive Telegram channel setup.
201+
fn setup_telegram(config: &mut Config) -> Result<()> {
202+
println!();
203+
println!("Telegram Bot Setup");
204+
println!("------------------");
205+
println!("To create a bot: Open Telegram, message @BotFather, send /newbot");
206+
println!();
207+
print!("Enter Telegram bot token (or press Enter to skip): ");
208+
io::stdout().flush()?;
209+
210+
let token = read_line()?;
211+
if token.is_empty() {
212+
println!(" Skipped.");
213+
return Ok(());
214+
}
215+
216+
let tg = config
217+
.channels
218+
.telegram
219+
.get_or_insert_with(Default::default);
220+
tg.token = token;
221+
tg.enabled = true;
222+
223+
print!("Allowlist user IDs/usernames (comma-separated, or Enter for all): ");
224+
io::stdout().flush()?;
225+
let allowlist = read_line()?;
226+
if !allowlist.is_empty() {
227+
tg.allow_from = allowlist
228+
.split(',')
229+
.map(|s| s.trim().to_string())
230+
.filter(|s| !s.is_empty())
231+
.collect();
232+
}
233+
234+
println!(" Telegram bot configured.");
235+
println!(" Run 'zeptoclaw gateway' to start the bot.");
236+
Ok(())
237+
}
238+
239+
/// Interactive Discord channel setup.
240+
fn setup_discord(config: &mut Config) -> Result<()> {
241+
println!();
242+
println!("Discord Bot Setup");
243+
println!("-----------------");
244+
println!("To create a bot:");
245+
println!(" 1. Go to https://discord.com/developers/applications");
246+
println!(" 2. Create New Application → Bot → Reset Token → copy it");
247+
println!(" 3. Enable MESSAGE CONTENT intent under Bot → Privileged Intents");
248+
println!(" 4. Invite bot to your server with OAuth2 URL Generator");
249+
println!();
250+
print!("Enter Discord bot token (or press Enter to skip): ");
251+
io::stdout().flush()?;
252+
253+
let token = read_line()?;
254+
if token.is_empty() {
255+
println!(" Skipped.");
256+
return Ok(());
257+
}
258+
259+
let dc = config.channels.discord.get_or_insert_with(Default::default);
260+
dc.token = token;
261+
dc.enabled = true;
262+
263+
print!("Allowlist user IDs (comma-separated, or Enter for all): ");
264+
io::stdout().flush()?;
265+
let allowlist = read_line()?;
266+
if !allowlist.is_empty() {
267+
dc.allow_from = allowlist
268+
.split(',')
269+
.map(|s| s.trim().to_string())
270+
.filter(|s| !s.is_empty())
271+
.collect();
272+
}
273+
274+
println!(" Discord bot configured.");
275+
println!(" Run 'zeptoclaw gateway' to start the bot.");
276+
Ok(())
277+
}
278+
279+
/// Interactive Slack channel setup.
280+
fn setup_slack(config: &mut Config) -> Result<()> {
281+
println!();
282+
println!("Slack Bot Setup");
283+
println!("---------------");
284+
println!("To create a bot:");
285+
println!(" 1. Go to https://api.slack.com/apps → Create New App");
286+
println!(" 2. Add Bot Token Scopes: chat:write, app_mentions:read");
287+
println!(" 3. Install to Workspace → copy Bot User OAuth Token (xoxb-...)");
288+
println!(" 4. Generate App-Level Token with connections:write scope");
289+
println!();
290+
print!("Enter Slack bot token (xoxb-..., or press Enter to skip): ");
291+
io::stdout().flush()?;
292+
293+
let bot_token = read_line()?;
294+
if bot_token.is_empty() {
295+
println!(" Skipped.");
296+
return Ok(());
297+
}
298+
299+
print!("Enter Slack app-level token (xapp-...): ");
300+
io::stdout().flush()?;
301+
let app_token = read_line()?;
302+
303+
let sl = config.channels.slack.get_or_insert_with(Default::default);
304+
sl.bot_token = bot_token;
305+
sl.app_token = app_token;
306+
sl.enabled = true;
307+
308+
println!(" Slack bot configured.");
309+
println!(" Run 'zeptoclaw gateway' to start the bot.");
310+
Ok(())
311+
}
312+
313+
/// Interactive Webhook channel setup.
314+
fn setup_webhook(config: &mut Config) -> Result<()> {
315+
println!();
316+
println!("Webhook Channel Setup");
317+
println!("---------------------");
318+
println!("Receives messages via HTTP POST to a local endpoint.");
319+
println!();
320+
321+
let wh = config.channels.webhook.get_or_insert_with(Default::default);
322+
323+
print!("Bind address [{}]: ", wh.bind_address);
324+
io::stdout().flush()?;
325+
let bind = read_line()?;
326+
if !bind.is_empty() {
327+
wh.bind_address = bind;
328+
}
329+
330+
print!("Port [{}]: ", wh.port);
331+
io::stdout().flush()?;
332+
let port_str = read_line()?;
333+
if !port_str.is_empty() {
334+
if let Ok(p) = port_str.parse::<u16>() {
335+
wh.port = p;
336+
} else {
337+
println!(" Invalid port, keeping default {}.", wh.port);
338+
}
339+
}
340+
341+
print!("Bearer auth token (or Enter for none): ");
342+
io::stdout().flush()?;
343+
let auth = read_line()?;
344+
if !auth.is_empty() {
345+
wh.auth_token = Some(auth);
346+
}
347+
348+
wh.enabled = true;
349+
println!(
350+
" Webhook configured at {}:{}{}",
351+
wh.bind_address, wh.port, wh.path
352+
);
353+
println!(" Run 'zeptoclaw gateway' to start listening.");
354+
Ok(())
355+
}
356+
212357
// ---------------------------------------------------------------------------
213358
// channel test
214359
// ---------------------------------------------------------------------------

src/cli/onboard.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,35 @@ pub(crate) async fn cmd_onboard(full: bool) -> Result<()> {
302302
println!(" Coding tools (grep, find) enabled.");
303303
}
304304

305+
// Ask about messaging channels
306+
println!();
307+
println!("Messaging Channels");
308+
println!("==================");
309+
println!("Connect ZeptoClaw to messaging platforms so you can chat via phone/desktop.");
310+
println!(" 1. Telegram (recommended — easiest setup)");
311+
println!(" 2. WhatsApp Web (native, QR code pairing)");
312+
println!(" 3. Discord");
313+
println!(" 4. Slack");
314+
println!(" 5. Skip (configure later with 'zeptoclaw channel setup <name>')");
315+
println!();
316+
print!("Which channel? [1/2/3/4/5]: ");
317+
io::stdout().flush()?;
318+
let channel_choice = read_line()?;
319+
match channel_choice.trim() {
320+
"1" => configure_telegram(&mut config)?,
321+
"2" => {
322+
if cfg!(feature = "whatsapp-web") {
323+
configure_whatsapp_channel(&mut config)?;
324+
} else {
325+
println!(" WhatsApp Web requires: cargo build --features whatsapp-web");
326+
println!(" Skipped.");
327+
}
328+
}
329+
"3" => configure_discord(&mut config)?,
330+
"4" => configure_slack(&mut config)?,
331+
_ => println!(" Skipped. Run 'zeptoclaw channel setup <name>' anytime."),
332+
}
333+
305334
// Save config
306335
config
307336
.save()
@@ -869,6 +898,74 @@ fn configure_whatsapp_channel(config: &mut Config) -> Result<()> {
869898
Ok(())
870899
}
871900

901+
/// Configure Discord channel.
902+
fn configure_discord(config: &mut Config) -> Result<()> {
903+
println!();
904+
println!("Discord Bot Setup");
905+
println!("-----------------");
906+
println!("To create a bot:");
907+
println!(" 1. Go to https://discord.com/developers/applications");
908+
println!(" 2. Create New Application → Bot → Reset Token → copy it");
909+
println!(" 3. Enable MESSAGE CONTENT intent under Bot → Privileged Intents");
910+
println!(
911+
" 4. Invite bot to your server with OAuth2 URL Generator (bot scope + Send Messages)"
912+
);
913+
println!();
914+
print!("Enter Discord bot token (or press Enter to skip): ");
915+
io::stdout().flush()?;
916+
917+
let token = read_line()?;
918+
919+
if !token.is_empty() {
920+
let discord_config = config.channels.discord.get_or_insert_with(Default::default);
921+
discord_config.token = token;
922+
discord_config.enabled = true;
923+
println!(" Discord bot configured.");
924+
println!(" Run 'zeptoclaw gateway' to start the bot.");
925+
} else {
926+
println!(" Skipped Discord configuration.");
927+
}
928+
929+
Ok(())
930+
}
931+
932+
/// Configure Slack channel.
933+
fn configure_slack(config: &mut Config) -> Result<()> {
934+
println!();
935+
println!("Slack Bot Setup");
936+
println!("---------------");
937+
println!("To create a bot:");
938+
println!(" 1. Go to https://api.slack.com/apps → Create New App");
939+
println!(" 2. Add Bot Token Scopes: chat:write, app_mentions:read");
940+
println!(" 3. Install to Workspace → copy Bot User OAuth Token (xoxb-...)");
941+
println!(
942+
" 4. Under Basic Information → App-Level Tokens → generate with connections:write scope"
943+
);
944+
println!();
945+
print!("Enter Slack bot token (xoxb-..., or press Enter to skip): ");
946+
io::stdout().flush()?;
947+
948+
let bot_token = read_line()?;
949+
950+
if bot_token.is_empty() {
951+
println!(" Skipped Slack configuration.");
952+
return Ok(());
953+
}
954+
955+
print!("Enter Slack app-level token (xapp-..., or press Enter to skip): ");
956+
io::stdout().flush()?;
957+
let app_token = read_line()?;
958+
959+
let slack_config = config.channels.slack.get_or_insert_with(Default::default);
960+
slack_config.bot_token = bot_token;
961+
slack_config.app_token = app_token;
962+
slack_config.enabled = true;
963+
println!(" Slack bot configured.");
964+
println!(" Run 'zeptoclaw gateway' to start the bot.");
965+
966+
Ok(())
967+
}
968+
872969
/// Configure runtime for shell command isolation.
873970
fn configure_runtime(config: &mut Config) -> Result<()> {
874971
println!();

0 commit comments

Comments
 (0)