merge: resolve conflicts between feat/whatsapp-email-channels and main

- Keep main's WhatsApp implementation (webhook-based, simpler)
- Preserve email channel fixes from our branch
- Merge all main branch updates (daemon, cron, health, etc.)
- Resolve Cargo.lock conflicts
This commit is contained in:
argenis de la rosa 2026-02-14 14:59:16 -05:00
commit 4e6da51924
40 changed files with 6925 additions and 780 deletions

View file

@ -1,3 +1,4 @@
use crate::config::schema::WhatsAppConfig;
use crate::config::{
AutonomyConfig, BrowserConfig, ChannelsConfig, ComposioConfig, Config, DiscordConfig,
HeartbeatConfig, IMessageConfig, MatrixConfig, MemoryConfig, ObservabilityConfig,
@ -91,6 +92,7 @@ pub fn run_wizard() -> Result<Config> {
observability: ObservabilityConfig::default(),
autonomy: AutonomyConfig::default(),
runtime: RuntimeConfig::default(),
reliability: crate::config::ReliabilityConfig::default(),
heartbeat: HeartbeatConfig::default(),
channels_config,
memory: MemoryConfig::default(), // SQLite + auto-save by default
@ -99,6 +101,7 @@ pub fn run_wizard() -> Result<Config> {
composio: composio_config,
secrets: secrets_config,
browser: BrowserConfig::default(),
identity: crate::config::IdentityConfig::default(),
};
println!(
@ -149,6 +152,61 @@ pub fn run_wizard() -> Result<Config> {
Ok(config)
}
/// Interactive repair flow: rerun channel setup only without redoing full onboarding.
pub fn run_channels_repair_wizard() -> Result<Config> {
println!("{}", style(BANNER).cyan().bold());
println!(
" {}",
style("Channels Repair — update channel tokens and allowlists only")
.white()
.bold()
);
println!();
let mut config = Config::load_or_init()?;
print_step(1, 1, "Channels (How You Talk to ZeroClaw)");
config.channels_config = setup_channels()?;
config.save()?;
println!();
println!(
" {} Channel config saved: {}",
style("").green().bold(),
style(config.config_path.display()).green()
);
let has_channels = config.channels_config.telegram.is_some()
|| config.channels_config.discord.is_some()
|| config.channels_config.slack.is_some()
|| config.channels_config.imessage.is_some()
|| config.channels_config.matrix.is_some();
if has_channels && config.api_key.is_some() {
let launch: bool = Confirm::new()
.with_prompt(format!(
" {} Launch channels now? (connected channels → AI → reply)",
style("🚀").cyan()
))
.default(true)
.interact()?;
if launch {
println!();
println!(
" {} {}",
style("").cyan(),
style("Starting channel server...").white().bold()
);
println!();
// Signal to main.rs to call start_channels after wizard returns
std::env::set_var("ZEROCLAW_AUTOSTART_CHANNELS", "1");
}
}
Ok(config)
}
// ── Quick setup (zero prompts) ───────────────────────────────────
/// Non-interactive setup: generates a sensible default config instantly.
@ -187,6 +245,7 @@ pub fn run_quick_setup(api_key: Option<&str>, provider: Option<&str>) -> Result<
observability: ObservabilityConfig::default(),
autonomy: AutonomyConfig::default(),
runtime: RuntimeConfig::default(),
reliability: crate::config::ReliabilityConfig::default(),
heartbeat: HeartbeatConfig::default(),
channels_config: ChannelsConfig::default(),
memory: MemoryConfig::default(),
@ -195,6 +254,7 @@ pub fn run_quick_setup(api_key: Option<&str>, provider: Option<&str>) -> Result<
composio: ComposioConfig::default(),
secrets: SecretsConfig::default(),
browser: BrowserConfig::default(),
identity: crate::config::IdentityConfig::default(),
};
config.save()?;
@ -204,7 +264,9 @@ pub fn run_quick_setup(api_key: Option<&str>, provider: Option<&str>) -> Result<
user_name: std::env::var("USER").unwrap_or_else(|_| "User".into()),
timezone: "UTC".into(),
agent_name: "ZeroClaw".into(),
communication_style: "Direct and concise".into(),
communication_style:
"Be warm, natural, and clear. Use occasional relevant emojis (1-2 max) and avoid robotic phrasing."
.into(),
};
scaffold_workspace(&workspace_dir, &default_ctx)?;
@ -878,24 +940,33 @@ fn setup_project_context() -> Result<ProjectContext> {
let style_options = vec![
"Direct & concise — skip pleasantries, get to the point",
"Friendly & casual — warm but efficient",
"Friendly & casual — warm, human, and helpful",
"Professional & polished — calm, confident, and clear",
"Expressive & playful — more personality + natural emojis",
"Technical & detailed — thorough explanations, code-first",
"Balanced — adapt to the situation",
"Custom — write your own style guide",
];
let style_idx = Select::new()
.with_prompt(" Communication style")
.items(&style_options)
.default(0)
.default(1)
.interact()?;
let communication_style = match style_idx {
0 => "Be direct and concise. Skip pleasantries. Get to the point.".to_string(),
1 => "Be friendly and casual. Warm but efficient.".to_string(),
2 => "Be technical and detailed. Thorough explanations, code-first.".to_string(),
_ => {
"Adapt to the situation. Be concise when needed, thorough when it matters.".to_string()
}
1 => "Be friendly, human, and conversational. Show warmth and empathy while staying efficient. Use natural contractions.".to_string(),
2 => "Be professional and polished. Stay calm, structured, and respectful. Use occasional tone-setting emojis only when appropriate.".to_string(),
3 => "Be expressive and playful when appropriate. Use relevant emojis naturally (0-2 max), and keep serious topics emoji-light.".to_string(),
4 => "Be technical and detailed. Thorough explanations, code-first.".to_string(),
5 => "Adapt to the situation. Default to warm and clear communication; be concise when needed, thorough when it matters.".to_string(),
_ => Input::new()
.with_prompt(" Custom communication style")
.default(
"Be warm, natural, and clear. Use occasional relevant emojis (1-2 max) and avoid robotic phrasing.".into(),
)
.interact_text()?,
};
println!(
@ -931,6 +1002,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
webhook: None,
imessage: None,
matrix: None,
whatsapp: None,
};
loop {
@ -975,6 +1047,14 @@ fn setup_channels() -> Result<ChannelsConfig> {
"— self-hosted chat"
}
),
format!(
"WhatsApp {}",
if config.whatsapp.is_some() {
"✅ connected"
} else {
"— Business Cloud API"
}
),
format!(
"Webhook {}",
if config.webhook.is_some() {
@ -989,7 +1069,7 @@ fn setup_channels() -> Result<ChannelsConfig> {
let choice = Select::new()
.with_prompt(" Connect a channel (or Done to continue)")
.items(&options)
.default(6)
.default(7)
.interact()?;
match choice {
@ -1041,17 +1121,38 @@ fn setup_channels() -> Result<ChannelsConfig> {
}
}
print_bullet(
"Allowlist your own Telegram identity first (recommended for secure + fast setup).",
);
print_bullet(
"Use your @username without '@' (example: argenis), or your numeric Telegram user ID.",
);
print_bullet("Use '*' only for temporary open testing.");
let users_str: String = Input::new()
.with_prompt(" Allowed usernames (comma-separated, or * for all)")
.default("*".into())
.with_prompt(
" Allowed Telegram identities (comma-separated: username without '@' and/or numeric user ID, '*' for all)",
)
.allow_empty(true)
.interact_text()?;
let allowed_users = if users_str.trim() == "*" {
vec!["*".into()]
} else {
users_str.split(',').map(|s| s.trim().to_string()).collect()
users_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
};
if allowed_users.is_empty() {
println!(
" {} No users allowlisted — Telegram inbound messages will be denied until you add your username/user ID or '*'.",
style("").yellow().bold()
);
}
config.telegram = Some(TelegramConfig {
bot_token: token,
allowed_users,
@ -1111,9 +1212,15 @@ fn setup_channels() -> Result<ChannelsConfig> {
.allow_empty(true)
.interact_text()?;
print_bullet("Allowlist your own Discord user ID first (recommended).");
print_bullet(
"Get it in Discord: Settings -> Advanced -> Developer Mode (ON), then right-click your profile -> Copy User ID.",
);
print_bullet("Use '*' only for temporary open testing.");
let allowed_users_str: String = Input::new()
.with_prompt(
" Allowed Discord user IDs (comma-separated, '*' for all, Enter to deny all)",
" Allowed Discord user IDs (comma-separated, recommended: your own ID, '*' for all)",
)
.allow_empty(true)
.interact_text()?;
@ -1214,9 +1321,15 @@ fn setup_channels() -> Result<ChannelsConfig> {
.allow_empty(true)
.interact_text()?;
print_bullet("Allowlist your own Slack member ID first (recommended).");
print_bullet(
"Member IDs usually start with 'U' (open your Slack profile -> More -> Copy member ID).",
);
print_bullet("Use '*' only for temporary open testing.");
let allowed_users_str: String = Input::new()
.with_prompt(
" Allowed Slack user IDs (comma-separated, '*' for all, Enter to deny all)",
" Allowed Slack user IDs (comma-separated, recommended: your own member ID, '*' for all)",
)
.allow_empty(true)
.interact_text()?;
@ -1378,6 +1491,90 @@ fn setup_channels() -> Result<ChannelsConfig> {
});
}
5 => {
// ── WhatsApp ──
println!();
println!(
" {} {}",
style("WhatsApp Setup").white().bold(),
style("— Business Cloud API").dim()
);
print_bullet("1. Go to developers.facebook.com and create a WhatsApp app");
print_bullet("2. Add the WhatsApp product and get your phone number ID");
print_bullet("3. Generate a temporary access token (System User)");
print_bullet("4. Configure webhook URL to: https://your-domain/whatsapp");
println!();
let access_token: String = Input::new()
.with_prompt(" Access token (from Meta Developers)")
.interact_text()?;
if access_token.trim().is_empty() {
println!(" {} Skipped", style("").dim());
continue;
}
let phone_number_id: String = Input::new()
.with_prompt(" Phone number ID (from WhatsApp app settings)")
.interact_text()?;
if phone_number_id.trim().is_empty() {
println!(" {} Skipped — phone number ID required", style("").dim());
continue;
}
let verify_token: String = Input::new()
.with_prompt(" Webhook verify token (create your own)")
.default("zeroclaw-whatsapp-verify".into())
.interact_text()?;
// Test connection
print!(" {} Testing connection... ", style("").dim());
let client = reqwest::blocking::Client::new();
let url = format!(
"https://graph.facebook.com/v18.0/{}",
phone_number_id.trim()
);
match client
.get(&url)
.header("Authorization", format!("Bearer {}", access_token.trim()))
.send()
{
Ok(resp) if resp.status().is_success() => {
println!(
"\r {} Connected to WhatsApp API ",
style("").green().bold()
);
}
_ => {
println!(
"\r {} Connection failed — check access token and phone number ID",
style("").red().bold()
);
continue;
}
}
let users_str: String = Input::new()
.with_prompt(
" Allowed phone numbers (comma-separated +1234567890, or * for all)",
)
.default("*".into())
.interact_text()?;
let allowed_numbers = if users_str.trim() == "*" {
vec!["*".into()]
} else {
users_str.split(',').map(|s| s.trim().to_string()).collect()
};
config.whatsapp = Some(WhatsAppConfig {
access_token: access_token.trim().to_string(),
phone_number_id: phone_number_id.trim().to_string(),
verify_token: verify_token.trim().to_string(),
allowed_numbers,
});
}
6 => {
// ── Webhook ──
println!();
println!(
@ -1432,6 +1629,9 @@ fn setup_channels() -> Result<ChannelsConfig> {
if config.matrix.is_some() {
active.push("Matrix");
}
if config.whatsapp.is_some() {
active.push("WhatsApp");
}
if config.webhook.is_some() {
active.push("Webhook");
}
@ -1618,7 +1818,7 @@ fn scaffold_workspace(workspace_dir: &Path, ctx: &ProjectContext) -> Result<()>
&ctx.timezone
};
let comm_style = if ctx.communication_style.is_empty() {
"Adapt to the situation. Be concise when needed, thorough when it matters."
"Be warm, natural, and clear. Use occasional relevant emojis (1-2 max) and avoid robotic phrasing."
} else {
&ctx.communication_style
};
@ -1667,6 +1867,14 @@ fn scaffold_workspace(workspace_dir: &Path, ctx: &ProjectContext) -> Result<()>
## Tools & Skills\n\n\
Skills are listed in the system prompt. Use `read` on a skill's SKILL.md for details.\n\
Keep local notes (SSH hosts, device names, etc.) in `TOOLS.md`.\n\n\
## Crash Recovery\n\n\
- If a run stops unexpectedly, recover context before acting.\n\
- Check `MEMORY.md` + latest `memory/*.md` notes to avoid duplicate work.\n\
- Resume from the last confirmed step, not from scratch.\n\n\
## Sub-task Scoping\n\n\
- Break complex work into focused sub-tasks with clear success criteria.\n\
- Keep sub-tasks small, verify each output, then merge results.\n\
- Prefer one clear objective per sub-task over broad \"do everything\" asks.\n\n\
## Make It Yours\n\n\
This is a starting point. Add your own conventions, style, and rules.\n"
);
@ -1704,6 +1912,11 @@ fn scaffold_workspace(workspace_dir: &Path, ctx: &ProjectContext) -> Result<()>
- Always introduce yourself as {agent} if asked\n\n\
## Communication\n\n\
{comm_style}\n\n\
- Sound like a real person, not a support script.\n\
- Mirror the user's energy: calm when serious, upbeat when casual.\n\
- Use emojis naturally (0-2 max when they help tone, not every sentence).\n\
- Match emoji density to the user. Formal user => minimal/no emojis.\n\
- Prefer specific, grounded phrasing over generic filler.\n\n\
## Boundaries\n\n\
- Private things stay private. Period.\n\
- When in doubt, ask before acting externally.\n\
@ -1744,11 +1957,23 @@ fn scaffold_workspace(workspace_dir: &Path, ctx: &ProjectContext) -> Result<()>
- Anything environment-specific\n\n\
## Built-in Tools\n\n\
- **shell** Execute terminal commands\n\
- Use when: running local checks, build/test commands, or diagnostics.\n\
- Don't use when: a safer dedicated tool exists, or command is destructive without approval.\n\
- **file_read** Read file contents\n\
- Use when: inspecting project files, configs, or logs.\n\
- Don't use when: you only need a quick string search (prefer targeted search first).\n\
- **file_write** Write file contents\n\
- Use when: applying focused edits, scaffolding files, or updating docs/code.\n\
- Don't use when: unsure about side effects or when the file should remain user-owned.\n\
- **memory_store** Save to memory\n\
- Use when: preserving durable preferences, decisions, or key context.\n\
- Don't use when: info is transient, noisy, or sensitive without explicit need.\n\
- **memory_recall** Search memory\n\
- **memory_forget** Delete a memory entry\n\n\
- Use when: you need prior decisions, user preferences, or historical context.\n\
- Don't use when: the answer is already in current files/conversation.\n\
- **memory_forget** Delete a memory entry\n\
- Use when: memory is incorrect, stale, or explicitly requested to be removed.\n\
- Don't use when: uncertain about impact; verify before deleting.\n\n\
---\n\
*Add whatever helps you do your job. This is your cheat sheet.*\n";
@ -2242,7 +2467,7 @@ mod tests {
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap();
assert!(
soul.contains("Adapt to the situation"),
soul.contains("Be warm, natural, and clear."),
"should default communication style"
);
}
@ -2383,6 +2608,31 @@ mod tests {
"TOOLS.md should list built-in tool: {tool}"
);
}
assert!(
tools.contains("Use when:"),
"TOOLS.md should include 'Use when' guidance"
);
assert!(
tools.contains("Don't use when:"),
"TOOLS.md should include 'Don't use when' guidance"
);
}
#[test]
fn soul_md_includes_emoji_awareness_guidance() {
let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default();
scaffold_workspace(tmp.path(), &ctx).unwrap();
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap();
assert!(
soul.contains("Use emojis naturally (0-2 max"),
"SOUL.md should include emoji usage guidance"
);
assert!(
soul.contains("Match emoji density to the user"),
"SOUL.md should include emoji-awareness guidance"
);
}
// ── scaffold_workspace: special characters in names ─────────
@ -2414,7 +2664,9 @@ mod tests {
user_name: "Argenis".into(),
timezone: "US/Eastern".into(),
agent_name: "Claw".into(),
communication_style: "Be friendly and casual. Warm but efficient.".into(),
communication_style:
"Be friendly, human, and conversational. Show warmth and empathy while staying efficient. Use natural contractions."
.into(),
};
scaffold_workspace(tmp.path(), &ctx).unwrap();
@ -2424,12 +2676,12 @@ mod tests {
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap();
assert!(soul.contains("You are **Claw**"));
assert!(soul.contains("Be friendly and casual"));
assert!(soul.contains("Be friendly, human, and conversational"));
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap();
assert!(user_md.contains("**Name:** Argenis"));
assert!(user_md.contains("**Timezone:** US/Eastern"));
assert!(user_md.contains("Be friendly and casual"));
assert!(user_md.contains("Be friendly, human, and conversational"));
let agents = fs::read_to_string(tmp.path().join("AGENTS.md")).unwrap();
assert!(agents.contains("Claw Personal Assistant"));