use crate::users::FieldUpdate;
use crate::{Role, Tool, Workspace};
use anyhow::{Context, anyhow};
use async_trait::async_trait;
use serde_json::json;
fn acting_user(ws: &Workspace) -> &str {
crate::users::personal_user_name(&ws.name).unwrap_or("admin")
}
fn err(msg: impl Into<String>) -> anyhow::Error {
anyhow!(msg.into())
}
pub(crate) struct SetupTelegramBotTool;
#[async_trait]
impl Tool for SetupTelegramBotTool {
fn name(&self) -> &'static str {
"setup_telegram_bot"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"bot_token": {
"type": "string",
"description": "The Telegram bot token from BotFather (the `NNN:AAA...` string)."
}
}),
&["bot_token"],
)
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, _ws: &Workspace, args: serde_json::Value) -> anyhow::Result<String> {
let token = super::get_str(&args, "bot_token")?;
crate::config::persist_settled_string_field("telegram_bot_token", token).await?;
Ok(
"Telegram bot token saved — the Telegram listener hot-reloads it immediately. \
Next, send `/start` to your bot in Telegram, then use `bind_telegram` to bind \
your @username so messages are routed to you."
.to_string(),
)
}
}
pub(crate) struct BindTelegramTool;
#[async_trait]
impl Tool for BindTelegramTool {
fn name(&self) -> &'static str {
"bind_telegram"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"telegram_handle": {
"type": "string",
"description": "The admin's Telegram @username (with or without the leading @)."
}
}),
&["telegram_handle"],
)
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, _ws: &Workspace, args: serde_json::Value) -> anyhow::Result<String> {
let user = "admin";
let handle = super::get_str(&args, "telegram_handle")?;
let store = crate::users::store();
let handle = store.validate_telegram_bind(user, handle).await?;
store.bind_channel(user, "telegram", &handle).await?;
store
.update_channel_contact("telegram", &handle, &handle)
.await?;
Ok(format!(
"Bound @{handle} to your account. Messages sent to the bot from that @username \
will now be routed to you."
))
}
}
pub(crate) struct AddWorkspaceTool;
#[async_trait]
impl Tool for AddWorkspaceTool {
fn name(&self) -> &'static str {
"add_workspace"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"name": {
"type": "string",
"description": "A short unique name for the workspace (used in ticket ids and the GUI)."
},
"path": {
"type": "string",
"description": "Absolute path to the project directory to manage."
}
}),
&["name", "path"],
)
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, ws: &Workspace, args: serde_json::Value) -> anyhow::Result<String> {
let name = super::get_str(&args, "name")?;
let path = super::get_str(&args, "path")?;
let store = crate::workspace::store();
store.add(name, path).await?;
crate::users::store()
.update_user(
acting_user(ws),
FieldUpdate::Unchanged,
FieldUpdate::Set(name),
FieldUpdate::Unchanged,
)
.await?;
Ok(format!(
"Registered workspace '{name}' at {path} and switched your active workspace to it. \
The workspace is being picked up — if the LLM provider is configured the pipeline \
will claim and discover it shortly."
))
}
}
pub(crate) struct AddUserTool;
#[async_trait]
impl Tool for AddUserTool {
fn name(&self) -> &'static str {
"add_user"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"name": {
"type": "string",
"description": "The new user's display name."
},
"telegram_handle": {
"type": "string",
"description": "The user's Telegram @username (with or without the leading @)."
},
"default_agent": {
"type": "string",
"description": "The default agent for this user: 'assistant' or 'artist'."
}
}),
&["name", "telegram_handle", "default_agent"],
)
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, _ws: &Workspace, args: serde_json::Value) -> anyhow::Result<String> {
let name = super::get_str(&args, "name")?;
let handle = super::get_str(&args, "telegram_handle")?;
let agent = super::get_str(&args, "default_agent")?
.parse::<Role>()
.context("default_agent must be 'assistant' or 'artist'")?;
if !matches!(agent, Role::Assistant | Role::Artist) {
return Err(err("default_agent must be 'assistant' or 'artist'"));
}
let store = crate::users::store();
let handle = store.validate_telegram_bind(name, handle).await?;
let mut existing_unbound = false;
if store.user_exists(name).await? {
let bound = store
.get_user_channels(name)
.await?
.iter()
.any(|c| c.channel == "telegram");
if bound {
return Err(err(format!("A user named '{name}' already exists")));
}
if store.get_permissions(name).await?.as_deref() == Some("full") {
return Err(err(format!(
"'{name}' is an admin — add_user only creates regular (non-admin) users"
)));
}
existing_unbound = true;
store
.update_user(
name,
FieldUpdate::Set(agent.as_str()),
FieldUpdate::Unchanged,
FieldUpdate::Unchanged,
)
.await?;
}
store.add_user(name, None, agent).await?;
store.bind_channel(name, "telegram", &handle).await?;
let role_note = "They are a regular (non-admin) user: they can chat with the \
Assistant and Artist agents only.";
if existing_unbound {
Ok(format!(
"Bound @{handle} to the existing user '{name}' and set their default agent to \
'{}'. {role_note}",
agent.as_str()
))
} else {
Ok(format!(
"Created user '{name}' with default agent '{}' and bound @{handle} to them. \
{role_note}",
agent.as_str()
))
}
}
}
pub(crate) struct SetupWebSearchTool;
#[async_trait]
impl Tool for SetupWebSearchTool {
fn name(&self) -> &'static str {
"setup_web_search"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"provider": {
"type": "string",
"description": "The web-search provider: 'firecrawl' or 'exa'."
},
"api_key": {
"type": "string",
"description": "The API key for the chosen provider."
}
}),
&["provider", "api_key"],
)
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, _ws: &Workspace, args: serde_json::Value) -> anyhow::Result<String> {
let provider = super::get_str(&args, "provider")?;
let provider = provider.to_ascii_lowercase();
let firecrawl = provider == "firecrawl";
if !firecrawl && provider != "exa" {
return Err(err("provider must be 'firecrawl' or 'exa'"));
}
let api_key = super::get_str(&args, "api_key")?;
crate::config::persist_settled_string_field("web_search_provider", &provider).await?;
crate::config::persist_settled_string_field(
if firecrawl {
"firecrawl_key"
} else {
"exa_key"
},
api_key,
)
.await?;
Ok(format!(
"Web-search backend registered: {provider}. Agents can now use `web_search`."
))
}
}
pub(crate) struct InstallChromeUseTool;
#[async_trait]
impl Tool for InstallChromeUseTool {
fn name(&self) -> &'static str {
"install_chrome_use"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(&json!({}), &[])
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, _ws: &Workspace, _args: serde_json::Value) -> anyhow::Result<String> {
#[cfg(target_os = "windows")]
{
Err(err(
"chrome-use install on Windows requires the Windows .exe — the curl|sh \
installer is macOS/Linux only. Download the .exe from the chrome-use releases, \
then install the Chrome extension manually from the Chrome Web Store (still \
required).",
))
}
#[cfg(not(target_os = "windows"))]
{
crate::tools::browser_daemon::install_chrome_use()
.await
.map_err(err)?;
Ok(
"chrome-use is installed: the CLI binary and skill were fetched by the \
install script, and the native-messaging host was registered via \
`chrome-use extension install`.\n\
One MANUAL step remains — you must install the chrome-use browser extension \
yourself:\n\
1. Open Chrome and go to the Chrome Web Store.\n\
2. Install the chrome-use extension (per its install docs).\n\
3. Pin it once installed so the native host can reach it.\n\
Only after that will agents be able to drive your normal browser via \
`browser`.\n\
Note: this setup is intentionally invasive — chrome-use gets full control \
over the user's real browser. It must only be run after you (the Support \
agent) have explained what it does and obtained the user's explicit consent."
.to_string(),
)
}
}
}
pub(crate) struct FinalizeTool;
#[async_trait]
impl Tool for FinalizeTool {
fn name(&self) -> &'static str {
"finalize"
}
fn parameters_schema(&self) -> serde_json::Value {
super::tool_params_schema(
&json!({
"agent": {
"type": "string",
"description": "The agent to switch to after onboarding: 'assistant', 'manager', or 'artist'."
}
}),
&["agent"],
)
}
fn should_scrub_output(&self, _args: &serde_json::Value) -> bool {
false
}
async fn execute(&self, ws: &Workspace, args: serde_json::Value) -> anyhow::Result<String> {
let user = acting_user(ws).to_string();
let agent = super::get_str(&args, "agent")?.parse::<Role>()?;
if !matches!(agent, Role::Assistant | Role::Manager | Role::Artist) {
return Err(err("agent must be 'assistant', 'manager', or 'artist'"));
}
crate::users::switch_active_role(&user, agent).await?;
crate::config::persist_settled_string_field(
crate::config::CONFIG_KEY_ONBOARDING_STATE,
crate::config::OnboardingState::Finished.as_str(),
)
.await?;
let label = crate::agent::role::role_info(&agent).display_label;
Ok(format!(
"Onboarding complete. You're now chatting with the {label} agent. Switch agents \
anytime from the role picker, or open Settings via the gear icon to adjust anything."
))
}
}