use anyhow::Result;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use tracing::{info, warn};
mod agent;
mod analyzer;
mod ai;
mod cli;
mod config;
mod github;
mod semantic;
mod storage;
mod team;
mod web;
mod tracking;
mod skills;
mod learning;
mod review;
mod vcs;
mod plugins;
mod sync;
mod monitor;
mod messaging;
mod automation;
mod snippets;
mod vuln;
mod share;
use cli::setup::SetupCommand;
use cli::query::QueryCommand;
#[derive(Parser)]
#[command(name = "i-self")]
#[command(about = "Your agentic developer companion - v0.3.0")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
#[arg(long, global = true)]
quiet: bool,
#[arg(long, global = true, action = clap::ArgAction::Count)]
verbose: u8,
}
fn log_filter_from_flags(quiet: bool, verbose: u8) -> &'static str {
if quiet {
"i_self=warn"
} else {
match verbose {
0 => "i_self=info",
1 => "i_self=debug",
_ => "i_self=trace",
}
}
}
#[derive(Subcommand)]
enum Commands {
Setup {
#[arg(long, env = "GITHUB_TOKEN")]
github_token: Option<String>,
#[arg(long)]
skip_github: bool,
#[arg(long)]
skip_local: bool,
},
Query {
query: String,
},
Status,
Refresh {
#[arg(long)]
full: bool,
},
Search {
query: String,
#[arg(short, long, default_value = "10")]
top_k: usize,
#[arg(short, long)]
language: Option<String>,
},
Ask {
question: String,
#[arg(long)]
rag: bool,
},
Index {
#[arg(default_value = ".")]
path: String,
#[arg(short, long)]
repo: Option<String>,
#[arg(long)]
no_cache: bool,
},
Team {
#[command(subcommand)]
command: TeamCommands,
},
Dashboard {
#[arg(short, long, default_value = "8080")]
port: u16,
},
Track {
#[command(subcommand)]
command: TrackingCommands,
},
Skills {
#[command(subcommand)]
command: SkillsCommands,
},
Learn {
#[command(subcommand)]
command: LearnCommands,
},
Review {
#[command(subcommand)]
command: ReviewCommands,
},
Api {
#[arg(short, long, default_value = "3000")]
port: u16,
},
Sync {
#[command(subcommand)]
command: SyncCommands,
},
Plugin {
#[command(subcommand)]
command: PluginCommands,
},
Monitor {
#[command(subcommand)]
command: MonitorCommands,
},
Message {
#[command(subcommand)]
command: MessageCommands,
},
Automate {
#[command(subcommand)]
command: AutomationCommands,
},
Snippet {
#[command(subcommand)]
command: SnippetCommands,
},
Vuln {
#[command(subcommand)]
command: VulnCommands,
},
Share {
#[command(subcommand)]
command: ShareCommands,
},
Completion {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
#[derive(Subcommand)]
enum ShareCommands {
Ls {
#[arg(short, long)]
provider: Option<String>,
#[arg(short, long)]
since: Option<String>,
#[arg(long)]
json: bool,
},
Export {
id: String,
#[arg(short, long, default_value = "markdown")]
format: String,
#[arg(long)]
redact: bool,
#[arg(short, long)]
output: Option<String>,
},
Upload {
id: String,
#[arg(short, long, default_value = "markdown")]
format: String,
#[arg(long)]
redact: bool,
#[arg(long, default_value_t = 86400)]
expires_in: u64,
},
Import {
input: String,
#[arg(short, long, default_value = "clipboard")]
target: String,
#[arg(long)]
project: Option<String>,
},
}
#[derive(Subcommand)]
enum VulnCommands {
Scan {
#[arg(short, long)]
path: Option<String>,
},
Check {
package: String,
version: String,
#[arg(short, long, default_value = "npm")]
ecosystem: String,
},
}
#[derive(Subcommand)]
enum SnippetCommands {
Ls {
#[arg(short, long)]
language: Option<String>,
#[arg(short, long)]
tag: Option<String>,
#[arg(short, long)]
favorites: bool,
#[arg(long, default_value = "10")]
limit: usize,
},
Add {
title: String,
code: String,
language: String,
#[arg(short, long)]
description: Option<String>,
#[arg(short, long)]
tags: Option<String>,
},
Search {
query: String,
},
Show {
id: String,
},
Rm {
id: String,
},
Update {
id: String,
#[arg(short, long)]
title: Option<String>,
#[arg(short, long)]
code: Option<String>,
#[arg(short, long)]
description: Option<String>,
},
TagAdd {
id: String,
tag: String,
},
TagRm {
id: String,
tag: String,
},
Favorite {
id: String,
},
Stats,
Languages,
Tags,
}
#[derive(Subcommand)]
enum AutomationCommands {
Ls,
Add {
name: String,
#[arg(short, long)]
trigger: String,
#[arg(short, long)]
duration: Option<u64>,
#[arg(short, long)]
action: String,
#[arg(short, long)]
message: Option<String>,
},
Rm {
id: String,
},
Enable {
id: String,
},
Disable {
id: String,
},
Show {
id: String,
},
Init,
Test {
#[arg(short, long)]
event: String,
#[arg(short, long, default_value = "0")]
value: i64,
},
}
#[derive(Subcommand)]
enum MessageCommands {
Send {
message: String,
},
Listen,
Config,
}
#[derive(Subcommand)]
enum MonitorCommands {
Start {
#[arg(short, long, default_value = "5")]
interval: u64,
},
Stop,
Status,
Suggestions,
Clear,
}
#[derive(Subcommand)]
enum TeamCommands {
Create {
name: String,
#[arg(short, long)]
description: Option<String>,
},
Add {
team: String,
username: String,
#[arg(short, long, default_value = "contributor")]
role: String,
},
Remove {
team: String,
username: String,
},
List {
team: String,
},
Aggregate {
team: String,
},
Ls,
}
#[derive(Subcommand)]
enum TrackingCommands {
Summary {
#[arg(short, long, default_value = "7")]
days: u32,
},
Start {
#[arg(short, long)]
project: Option<String>,
#[arg(short, long)]
language: Option<String>,
},
End,
Patterns {
#[arg(short, long, default_value = "30")]
days: u32,
},
}
#[derive(Subcommand)]
enum SkillsCommands {
Show,
Analyze {
#[arg(short, long)]
job: String,
},
Jobs,
}
#[derive(Subcommand)]
enum LearnCommands {
Job {
#[arg(short, long)]
job: String,
},
Skill {
#[arg(short, long)]
skill: String,
#[arg(short, long)]
target_level: Option<String>,
},
}
#[derive(Subcommand)]
enum ReviewCommands {
File {
#[arg(short, long)]
path: String,
#[arg(short, long, default_value = "rust")]
language: String,
},
Code {
#[arg(short, long)]
code: String,
#[arg(short, long, default_value = "rust")]
language: String,
},
}
#[derive(Subcommand)]
enum SyncCommands {
Push,
Pull,
Status,
}
#[derive(Subcommand)]
enum PluginCommands {
List,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let default_filter = log_filter_from_flags(cli.quiet, cli.verbose);
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
tracing_subscriber::fmt().with_env_filter(env_filter).init();
match cli.command {
Commands::Setup { github_token, skip_github, skip_local } => {
info!("Starting i-self setup...");
let setup = SetupCommand::new(github_token, skip_github, skip_local);
setup.run().await?;
}
Commands::Query { query } => {
let query_cmd = QueryCommand::new();
query_cmd.run(&query).await?;
}
Commands::Status => {
show_status().await?;
}
Commands::Refresh { full } => {
refresh_knowledge(full).await?;
}
Commands::Search { query, top_k, language } => {
semantic_search(&query, top_k, language).await?;
}
Commands::Ask { question, rag } => {
ai_ask(&question, rag).await?;
}
Commands::Index { path, repo, no_cache } => {
index_code(&path, repo, no_cache).await?;
}
Commands::Team { command } => {
handle_team_command(command).await?;
}
Commands::Dashboard { port } => {
start_dashboard(port).await?;
}
Commands::Track { command } => {
handle_tracking_command(command).await?;
}
Commands::Skills { command } => {
handle_skills_command(command).await?;
}
Commands::Learn { command } => {
handle_learning_command(command).await?;
}
Commands::Review { command } => {
handle_review_command(command).await?;
}
Commands::Api { port } => {
handle_api_command(port).await?;
}
Commands::Sync { command } => {
handle_sync_command(command).await?;
}
Commands::Plugin { command } => {
handle_plugin_command(command).await?;
}
Commands::Monitor { command } => {
handle_monitor_command(command).await?;
}
Commands::Message { command } => {
handle_message_command(command).await?;
}
Commands::Automate { command } => {
handle_automation_command(command).await?;
}
Commands::Snippet { command } => {
handle_snippet_command(command).await?;
}
Commands::Share { command } => {
handle_share_command(command).await?;
}
Commands::Completion { shell } => {
use clap::CommandFactory;
let mut cmd = Cli::command();
let bin_name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, bin_name, &mut std::io::stdout());
}
Commands::Vuln { command } => {
handle_vuln_command(command).await?;
}
}
Ok(())
}
async fn show_status() -> Result<()> {
let storage = storage::Storage::new()?;
let profile = storage.load_profile().await?;
println!("{}", cli::format::format_profile(&profile));
Ok(())
}
async fn refresh_knowledge(full: bool) -> Result<()> {
let _storage = storage::Storage::new()?;
if full {
warn!("Performing full refresh - this may take a while...");
let setup = SetupCommand::new(None, false, false);
setup.run().await?;
} else {
info!("Performing incremental refresh...");
let agent_system = agent::AgentSystem::new();
agent_system.refresh_recent().await?;
}
println!("Knowledge base updated successfully!");
Ok(())
}
async fn semantic_search(query: &str, top_k: usize, language: Option<String>) -> Result<()> {
use semantic::{EmbeddingGenerator, EmbeddingStorage, SemanticConfig, SemanticSearch};
println!("🔍 Searching: {}", query);
let storage = storage::Storage::new()?;
let embedding_storage = EmbeddingStorage::new(
storage.base_dir(),
SemanticConfig::default()
)?;
let probe = EmbeddingGenerator::new(SemanticConfig::default())?;
let embeddings = embedding_storage.load_embeddings_for(&probe.id()).await?;
if embeddings.is_empty() {
println!("⚠️ No (matching) code indexed yet. Run 'i-self index' to rebuild for the current embedder.");
return Ok(());
}
let search = SemanticSearch::new(SemanticConfig::default(), embeddings)?;
let results = if let Some(lang) = language {
search.search_filtered(query, top_k, Some(&lang), None, None).await?
} else {
search.search(query, top_k).await?
};
println!("\n📊 Found {} results:\n", results.len());
for (i, result) in results.iter().enumerate() {
println!("{}. {} ({} - {:.1}% match)",
i + 1,
result.embedding.metadata.source_file,
result.embedding.metadata.language,
result.score * 100.0
);
println!(" Repository: {}", result.embedding.metadata.repository);
let preview: String = result.embedding.content.lines()
.take(3)
.collect::<Vec<_>>()
.join("\n");
println!(" Preview:\n {}\n", preview);
}
Ok(())
}
async fn ai_ask(question: &str, use_rag: bool) -> Result<()> {
use ai::{AIConfig, AIProvider, CodeAssistant};
println!("🤖 Asking AI: {}\n", question);
let config = AIConfig {
provider: AIProvider::OpenAI,
api_key: std::env::var("OPENAI_API_KEY").unwrap_or_default(),
api_base: None,
model: "gpt-4".to_string(),
max_tokens: 2000,
temperature: 0.7,
system_prompt: None,
timeout_seconds: 60,
retry_attempts: 3,
};
if config.api_key.is_empty() {
println!("⚠️ Set OPENAI_API_KEY environment variable to use AI features.");
return Ok(());
}
let assistant = CodeAssistant::new(config)?;
let answer = if use_rag {
use semantic::{EmbeddingGenerator, EmbeddingStorage, SemanticConfig};
let storage = storage::Storage::new()?;
let embedding_storage = EmbeddingStorage::new(
storage.base_dir(),
SemanticConfig::default()
)?;
let probe = EmbeddingGenerator::new(SemanticConfig::default())?;
let embeddings = embedding_storage.load_embeddings_for(&probe.id()).await?;
let search = semantic::SemanticSearch::new(
SemanticConfig::default(),
embeddings
)?;
let results = search.search(question, 3).await?;
let context_chunks: Vec<String> = results.into_iter()
.map(|r| r.embedding.content)
.collect();
let profile = storage.load_profile().await.ok();
let rag_query = ai::RAGQuery {
query: question.to_string(),
context_chunks,
developer_profile: profile,
};
assistant.ask_with_rag(&rag_query).await?
} else {
assistant.ask(question, None).await?
};
println!("{}\n", answer);
Ok(())
}
async fn index_code(path: &str, repo: Option<String>, no_cache: bool) -> Result<()> {
use semantic::{EmbeddingStorage, SemanticConfig, SemanticIndex};
let path = std::path::Path::new(path);
let repo_name = repo.unwrap_or_else(|| {
path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string()
});
println!("📁 Indexing: {} (repository: {})", path.display(), repo_name);
let config = SemanticConfig::default();
let index = SemanticIndex::new(config.clone())?;
let probe = semantic::EmbeddingGenerator::new(config.clone())?;
let embedder_id = probe.id();
let storage = storage::Storage::new()?;
let embedding_storage = EmbeddingStorage::new(storage.base_dir(), config)?;
let (embeddings, hits, misses) = if no_cache {
let v = index.index_directory(path, &repo_name).await?;
let total = v.len();
(v, 0, total)
} else {
let mut cache = embedding_storage.load_cache(&embedder_id).await;
let result = index
.index_directory_incremental(path, &repo_name, &mut cache)
.await?;
embedding_storage.save_cache(&cache).await?;
result
};
if !no_cache {
println!(
" cache: {} files reused, {} re-embedded",
hits, misses
);
}
embedding_storage
.save_embeddings_with_id(&embeddings, &embedder_id)
.await?;
println!(
"✅ Indexed {} code chunks (embedder: {})",
embeddings.len(),
embedder_id
);
Ok(())
}
async fn handle_team_command(command: TeamCommands) -> Result<()> {
use team::TeamManager;
let storage = storage::Storage::new()?;
let manager = TeamManager::new(storage.base_dir())?;
match command {
TeamCommands::Create { name, description } => {
manager.create_team(&name, description.as_deref())?;
println!("✅ Created team: {}", name);
}
TeamCommands::Add { team, username, role } => {
let role = match role.as_str() {
"lead" => team::TeamRole::Lead,
"senior" => team::TeamRole::Senior,
"mid" => team::TeamRole::Mid,
"junior" => team::TeamRole::Junior,
_ => team::TeamRole::Contributor,
};
manager.add_member(&team, &username, role, None)?;
println!("✅ Added {} to team {}", username, team);
}
TeamCommands::Remove { team, username } => {
manager.remove_member(&team, &username)?;
println!("✅ Removed {} from team {}", username, team);
}
TeamCommands::List { team } => {
let config = manager.load_team(&team)?;
println!("\n👥 Team: {}", config.name);
if let Some(desc) = config.description {
println!(" {}", desc);
}
println!("\nMembers:");
for member in &config.members {
println!(" - {} ({})", member.github_username, member.role);
}
}
TeamCommands::Aggregate { team } => {
use team::TeamAggregator;
let config = manager.load_team(&team)?;
let profiles = manager.load_member_profiles(&team).await?;
let aggregator = TeamAggregator::new();
let profile = aggregator.aggregate(&config, &profiles)?;
let team_path = storage.base_dir().join("teams").join(format!("{}_profile.json", team));
let content = serde_json::to_string_pretty(&profile)?;
tokio::fs::write(&team_path, content).await?;
println!("✅ Aggregated team profile for: {}", team);
println!(" Languages: {}", profile.language_coverage.len());
println!(" Technologies: {}", profile.technology_coverage.len());
println!(" Recommendations: {}", profile.recommendations.len());
}
TeamCommands::Ls => {
let teams = manager.list_teams()?;
if teams.is_empty() {
println!("No teams found. Create one with 'i-self team create <name>'");
} else {
println!("Teams:");
for team in teams {
println!(" - {}", team);
}
}
}
}
Ok(())
}
async fn start_dashboard(port: u16) -> Result<()> {
use web::AppState;
println!("🚀 Starting i-self dashboard on http://localhost:{}", port);
println!("Press Ctrl+C to stop\n");
let state = AppState::new().await?;
web::start_dashboard(port, state).await?;
Ok(())
}
async fn handle_tracking_command(command: TrackingCommands) -> Result<()> {
use cli::TrackingCommand;
match command {
TrackingCommands::Summary { days } => {
let cmd = TrackingCommand {
days,
start: false,
end: false,
project: None,
language: None,
};
cmd.run().await?;
}
TrackingCommands::Start { project, language } => {
let cmd = TrackingCommand {
days: 7,
start: true,
end: false,
project,
language,
};
cmd.run().await?;
}
TrackingCommands::End => {
let cmd = TrackingCommand {
days: 7,
start: false,
end: true,
project: None,
language: None,
};
cmd.run().await?;
}
TrackingCommands::Patterns { days } => {
let cmd = TrackingCommand {
days,
start: false,
end: false,
project: None,
language: None,
};
cmd.run().await?;
}
}
Ok(())
}
async fn handle_skills_command(command: SkillsCommands) -> Result<()> {
use cli::SkillsCommand;
match command {
SkillsCommands::Show => {
let cmd = SkillsCommand {
job: None,
list_jobs: false,
add_skill: None,
level: None,
};
cmd.run().await?;
}
SkillsCommands::Analyze { job } => {
let cmd = SkillsCommand {
job: Some(job),
list_jobs: false,
add_skill: None,
level: None,
};
cmd.run().await?;
}
SkillsCommands::Jobs => {
let cmd = SkillsCommand {
job: None,
list_jobs: true,
add_skill: None,
level: None,
};
cmd.run().await?;
}
}
Ok(())
}
async fn handle_learning_command(command: LearnCommands) -> Result<()> {
use cli::LearningCommand;
match command {
LearnCommands::Job { job } => {
let cmd = LearningCommand {
job: Some(job),
skill: None,
target_level: None,
};
cmd.run().await?;
}
LearnCommands::Skill { skill, target_level } => {
let cmd = LearningCommand {
job: None,
skill: Some(skill),
target_level,
};
cmd.run().await?;
}
}
Ok(())
}
async fn handle_review_command(command: ReviewCommands) -> Result<()> {
use cli::ReviewCommand;
match command {
ReviewCommands::File { path, language } => {
let cmd = ReviewCommand {
file: Some(path),
code: None,
language,
review_type: "self".to_string(),
context: None,
use_patterns: false,
};
cmd.run().await?;
}
ReviewCommands::Code { code, language } => {
let cmd = ReviewCommand {
file: None,
code: Some(code),
language,
review_type: "self".to_string(),
context: None,
use_patterns: false,
};
cmd.run().await?;
}
}
Ok(())
}
async fn handle_api_command(port: u16) -> Result<()> {
println!("Starting API server on http://localhost:{} (alias for `dashboard`)", port);
start_dashboard(port).await
}
async fn handle_sync_command(command: SyncCommands) -> Result<()> {
use crate::config::AppConfig;
use crate::sync::{CloudSync, SyncConfig, SyncProvider};
let data_dir = dirs::data_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("i-self");
let app_cfg = AppConfig::load().unwrap_or_default();
let cloud = app_cfg.cloud;
let bucket = std::env::var("ISELF_SYNC_BUCKET")
.ok()
.or(cloud.bucket)
.unwrap_or_default();
let endpoint = std::env::var("ISELF_SYNC_ENDPOINT")
.ok()
.or(cloud.endpoint)
.filter(|s| !s.is_empty());
let region = std::env::var("ISELF_SYNC_REGION")
.ok()
.or(cloud.region)
.unwrap_or_else(|| "us-east-1".to_string());
let access_key_id = std::env::var("AWS_ACCESS_KEY_ID")
.ok()
.or(cloud.access_key);
let secret_access_key = std::env::var("AWS_SECRET_ACCESS_KEY")
.ok()
.or(cloud.secret_key);
let prefix = std::env::var("ISELF_SYNC_PREFIX").ok().or(cloud.prefix);
let force_path_style = endpoint.is_some()
&& std::env::var("ISELF_SYNC_VHOST_STYLE").ok().as_deref() != Some("1");
let provider = if bucket.is_empty() {
SyncProvider::None
} else {
SyncProvider::S3
};
let config = SyncConfig {
enabled: !bucket.is_empty(),
provider,
bucket,
endpoint,
region,
access_key_id,
secret_access_key,
prefix,
force_path_style,
auto_sync: false,
sync_interval_minutes: 60,
};
let sync = CloudSync::new(data_dir, config);
match command {
SyncCommands::Push => {
if !sync.is_enabled() {
eprintln!(
"Cloud sync is not configured. Set [cloud] bucket in ~/.i-self/config.toml \
or ISELF_SYNC_BUCKET=<name>; for MinIO/R2/Spaces also set \
ISELF_SYNC_ENDPOINT=<url>. Credentials come from AWS_ACCESS_KEY_ID / \
AWS_SECRET_ACCESS_KEY (or the standard AWS credential chain)."
);
return Ok(());
}
println!("Uploading to {}...", sync.get_config().bucket);
let status = sync.sync().await?;
println!(
"Sync complete: {} of {} files uploaded ({} failed)",
status.synced_files, status.total_files, status.pending_files
);
for err in status.errors.iter().take(5) {
eprintln!(" ! {}", err);
}
}
SyncCommands::Pull => {
if !sync.is_enabled() {
eprintln!("Cloud sync is not configured.");
return Ok(());
}
println!("Downloading from {}...", sync.get_config().bucket);
let status = sync.download().await?;
println!(
"Download complete: {} of {} objects ({} failed)",
status.synced_files, status.total_files, status.pending_files
);
for err in status.errors.iter().take(5) {
eprintln!(" ! {}", err);
}
}
SyncCommands::Status => {
let cfg = sync.get_config();
if sync.is_enabled() {
println!("Cloud sync: enabled");
println!(" bucket: {}", cfg.bucket);
println!(
" endpoint: {}",
cfg.endpoint.as_deref().unwrap_or("(AWS S3 default)")
);
println!(" region: {}", cfg.region);
if let Some(p) = &cfg.prefix {
println!(" prefix: {}", p);
}
} else {
println!(
"Cloud sync is not configured. Set ISELF_SYNC_BUCKET (and \
ISELF_SYNC_ENDPOINT for non-AWS providers like MinIO)."
);
}
}
}
Ok(())
}
async fn handle_plugin_command(command: PluginCommands) -> Result<()> {
use crate::plugins::PluginManager;
match command {
PluginCommands::List => {
let mut manager = PluginManager::new();
manager.register_builtin_plugins();
println!("\n=== Built-in analyzers ===\n");
for plugin in &manager.plugins {
println!(" - {} (priority: {})", plugin.name(), plugin.priority());
}
println!(
"\nThese are compiled into the binary. There is no dynamic plugin loader; \
to add a new analyzer, edit src/plugins/mod.rs and rebuild."
);
}
}
Ok(())
}
async fn handle_monitor_command(command: MonitorCommands) -> Result<()> {
use crate::monitor::{ActivityMonitor, MonitorConfig};
use std::sync::Arc;
use tokio::sync::RwLock;
static MONITOR: std::sync::OnceLock<Arc<RwLock<Option<ActivityMonitor>>>> = std::sync::OnceLock::new();
let monitor = MONITOR.get_or_init(|| Arc::new(RwLock::new(None)));
match command {
MonitorCommands::Start { interval } => {
let mut config = MonitorConfig::default();
config.screenshot_interval_secs = interval;
let mon = ActivityMonitor::new(config)?;
mon.start().await?;
*monitor.write().await = Some(mon);
println!("✅ Activity monitor started");
println!(" Screenshot interval: {} seconds", interval);
println!(" Tracking: keyboard, mouse, window, idle");
println!(" Use 'i-self monitor status' to check activity");
println!(" Use 'i-self monitor suggestions' to see AI suggestions");
}
MonitorCommands::Stop => {
if let Some(mon) = monitor.write().await.take() {
mon.stop().await;
println!("✅ Activity monitor stopped");
} else {
println!("Monitor is not running");
}
}
MonitorCommands::Status => {
if let Some(ref mon) = *monitor.read().await {
let snapshot = mon.get_current_snapshot().await?;
println!("\n=== Current Activity ===\n");
println!("Active Window: {:?}", snapshot.active_window);
println!("Keyboard Events: {}", snapshot.keyboard_events);
println!("Mouse Clicks: {}", snapshot.mouse_clicks);
println!("Mouse Moves: {}", snapshot.mouse_moves);
println!("Idle: {}", snapshot.is_idle);
if let Some(ref path) = snapshot.screenshot_path {
println!("Latest Screenshot: {}", path.display());
}
} else {
println!("Monitor is not running. Use 'i-self monitor start' to start.");
}
}
MonitorCommands::Suggestions => {
if let Some(ref mon) = *monitor.read().await {
let suggestions = mon.get_suggestions().await;
if suggestions.is_empty() {
println!("No suggestions yet. Keep working!");
} else {
println!("\n=== AI Suggestions ===\n");
for (idx, s) in suggestions.iter().enumerate() {
println!("{}. [{}] {}", idx + 1, format!("{:?}", s.category), s.title);
println!(" {}", s.description);
println!(" Confidence: {:.0}%", s.confidence * 100.0);
println!();
}
}
} else {
println!("Monitor is not running. Use 'i-self monitor start' to start.");
}
}
MonitorCommands::Clear => {
if let Some(ref mon) = *monitor.read().await {
mon.clear_suggestions().await;
println!("✅ Suggestions cleared");
} else {
println!("Monitor is not running");
}
}
}
Ok(())
}
async fn handle_message_command(command: MessageCommands) -> Result<()> {
use crate::messaging::{MessagingConfig, MessagingManager, MessagingProvider};
match command {
MessageCommands::Send { message } => {
let telegram_token = std::env::var("TELEGRAM_BOT_TOKEN").ok();
let whatsapp_key = std::env::var("WHATSAPP_API_KEY").ok();
let whatsapp_phone = std::env::var("WHATSAPP_PHONE").ok();
let provider = if telegram_token.is_some() {
MessagingProvider::Telegram
} else if whatsapp_key.is_some() {
MessagingProvider::WhatsApp
} else {
println!("⚠️ No messaging provider configured");
println!("Set TELEGRAM_BOT_TOKEN or WHATSAPP_API_KEY");
return Ok(());
};
let config = MessagingConfig {
enabled: true,
provider,
telegram_bot_token: telegram_token,
whatsapp_api_key: whatsapp_key,
whatsapp_phone_number: whatsapp_phone,
allowed_chat_ids: vec![],
commands_enabled: true,
status_notifications: false,
};
let manager = MessagingManager::new(config)?;
if manager.is_enabled() {
manager.send_status(&message).await?;
println!("✅ Message sent");
}
}
MessageCommands::Listen => {
use crate::messaging::listener::{run, IselfBotContext, ListenerConfig};
use std::sync::Arc;
let storage = storage::Storage::new()?;
let cfg = ListenerConfig::from_env(storage.base_dir().to_path_buf())?;
let ctx = Arc::new(IselfBotContext::new(storage));
println!(
"📱 Telegram bot listening — {} chat(s) allowlisted. Ctrl+C to stop.",
cfg.allowed_chat_ids.len()
);
run(cfg, ctx).await?;
}
MessageCommands::Config => {
println!("\n=== Messaging Configuration ===\n");
println!("Telegram: Set TELEGRAM_BOT_TOKEN");
println!("WhatsApp: Set WHATSAPP_API_KEY and WHATSAPP_PHONE");
}
}
Ok(())
}async fn handle_automation_command(command: AutomationCommands) -> Result<()> {
use crate::automation::{AutomationConfig, AutomationEngine, AutomationRule, RuleAction, TriggerType};
match command {
AutomationCommands::Ls => {
let config = AutomationConfig::load().unwrap_or_default();
println!("\n=== Automation Rules ===\n");
if config.rules.is_empty() {
println!("No rules configured. Run 'i-self automate init' to add defaults.");
return Ok(());
}
for rule in &config.rules {
let status = if rule.enabled { "[+]" } else { "[-]" };
let trigger_desc = match &rule.trigger {
TriggerType::MeetingSilence { duration_secs } => format!("meeting silence > {}s", duration_secs),
TriggerType::Idle { duration_secs } => format!("idle > {}s", duration_secs),
TriggerType::NoActivity { duration_secs } => format!("no activity > {}s", duration_secs),
TriggerType::ActivitySpike { threshold, .. } => format!("activity spike > {}", threshold),
TriggerType::TimeOfDay { hour, minute } => format!("time {:02}:{:02}", hour, minute),
TriggerType::IncomingCall => "incoming call".to_string(),
TriggerType::Custom { event } => format!("custom: {}", event),
};
println!("{} [{}] {}", status, &rule.id[..8], rule.name);
println!(" Trigger: {}", trigger_desc);
println!(" Description: {}", rule.description);
println!();
}
}
AutomationCommands::Add { name, trigger, duration, action, message } => {
let duration = duration.unwrap_or(60);
let trigger = match trigger.as_str() {
"idle" => TriggerType::Idle { duration_secs: duration },
"meeting-silence" => TriggerType::MeetingSilence { duration_secs: duration },
"no-activity" => TriggerType::NoActivity { duration_secs: duration },
"activity-spike" => TriggerType::ActivitySpike { threshold: duration as u32, window_secs: 60 },
"incoming-call" => TriggerType::IncomingCall,
_ => {
println!("Unknown trigger type: {}", trigger);
println!("Valid types: idle, meeting-silence, no-activity, activity-spike, incoming-call");
return Ok(());
}
};
let rule_action = match action.as_str() {
"notify" => RuleAction::Notify(message.clone().unwrap_or_else(|| "Notification".to_string())),
"exit-meeting" => RuleAction::ExitMeeting,
"log" => RuleAction::Log(message.clone().unwrap_or_else(|| "Log message".to_string())),
"telegram" => RuleAction::SendToMobile {
message: message.clone().unwrap_or_else(|| "Alert from i-self".to_string()),
provider: crate::automation::MobileProvider::Telegram
},
"whatsapp" => RuleAction::SendToMobile {
message: message.clone().unwrap_or_else(|| "Alert from i-self".to_string()),
provider: crate::automation::MobileProvider::WhatsApp
},
_ => {
println!("Unknown action: {}", action);
println!("Valid actions: notify, exit-meeting, log, telegram, whatsapp");
return Ok(());
}
};
let mut config = AutomationConfig::load().unwrap_or_default();
let rule = AutomationRule::new(&name, trigger, vec![rule_action]);
config.add_rule(rule.clone());
config.save()?;
println!("Added rule: {} (ID: {})", rule.name, &rule.id[..8]);
}
AutomationCommands::Rm { id } => {
let mut config = AutomationConfig::load()?;
config.remove_rule(&id)?;
config.save()?;
println!("Removed rule: {}", &id[..8]);
}
AutomationCommands::Enable { id } => {
let mut config = AutomationConfig::load()?;
config.enable_rule(&id)?;
config.save()?;
println!("Enabled rule: {}", &id[..8]);
}
AutomationCommands::Disable { id } => {
let mut config = AutomationConfig::load()?;
config.disable_rule(&id)?;
config.save()?;
println!("Disabled rule: {}", &id[..8]);
}
AutomationCommands::Show { id } => {
let config = AutomationConfig::load()?;
let rule = config.rules.iter().find(|r| r.id == id);
match rule {
Some(r) => {
println!("\n=== Rule: {} ===\n", r.name);
println!("ID: {}", r.id);
println!("Enabled: {}", r.enabled);
println!("Description: {}", r.description);
println!("Cooldown: {}s", r.cooldown_secs);
println!("Last triggered: {:?}", r.last_triggered);
}
None => println!("Rule not found: {}", &id[..8]),
}
}
AutomationCommands::Init => {
let config = AutomationConfig::default_rules();
config.save()?;
println!("Initialized {} default rules", config.rules.len());
}
AutomationCommands::Test { event, value } => {
let mut engine = AutomationEngine::new();
let actions = engine.check_triggers(&event, value);
println!("\n=== Test Results ===");
println!("Event: {} (value: {})", event, value);
if actions.is_empty() {
println!("No actions triggered.");
} else {
println!("Triggered {} action(s):", actions.len());
for action in &actions {
println!(" - {:?}", action);
}
if let Err(e) = engine.execute_actions(&actions).await {
eprintln!("execute_actions failed: {}", e);
}
}
}
}
Ok(())
}async fn handle_snippet_command(command: SnippetCommands) -> Result<()> {
use crate::snippets::SnippetManager;
match command {
SnippetCommands::Ls { language, tag, favorites, limit } => {
let manager = SnippetManager::new()?;
let snippets = if let Some(lang) = language {
manager.search_by_language(&lang)
} else if let Some(t) = tag {
manager.search_by_tag(&t)
} else if favorites {
manager.favorites()
} else {
manager.recent(limit)
};
println!("\n=== Code Snippets ===\n");
if snippets.is_empty() {
println!("No snippets found. Add one with: i-self snippet add");
return Ok(());
}
for s in snippets {
let fav = if s.favorite { "⭐" } else { " " };
println!("{} [{}] {} ({})", fav, &s.id[..8], s.title, s.language);
if !s.tags.is_empty() {
println!(" Tags: {}", s.tags.join(", "));
}
println!();
}
}
SnippetCommands::Add { title, code, language, description, tags } => {
let mut manager = SnippetManager::new()?;
let tag_vec: Vec<String> = tags
.map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
.unwrap_or_default();
let tag_refs: Vec<&str> = tag_vec.iter().map(|s| s.as_str()).collect();
let snippet = manager.add(&title, &code, &language, description.as_deref(), tag_refs)?;
println!("✅ Added snippet: {} (ID: {})", snippet.title, &snippet.id[..8]);
}
SnippetCommands::Search { query } => {
let manager = SnippetManager::new()?;
let results = manager.search(&query);
println!("\n=== Search Results for '{}' ===\n", query);
if results.is_empty() {
println!("No matches found.");
return Ok(());
}
for s in results {
println!("[{}] {} ({})", &s.id[..8], s.title, s.language);
println!("{}", &s.code[..s.code.len().min(100)]);
if !s.description.is_empty() {
println!(" {}", s.description);
}
println!();
}
}
SnippetCommands::Show { id } => {
let manager = SnippetManager::new()?;
if let Some(s) = manager.get(&id) {
println!("\n=== {} ===\n", s.title);
println!("ID: {}", s.id);
println!("Language: {}", s.language);
println!("Tags: {}", s.tags.join(", "));
println!("Usage: {} times", s.usage_count);
println!("Favorite: {}", if s.favorite { "⭐ Yes" } else { "No" });
println!("Created: {}", chrono::DateTime::from_timestamp(s.created_at, 0).unwrap_or_default());
println!("Updated: {}", chrono::DateTime::from_timestamp(s.updated_at, 0).unwrap_or_default());
if !s.description.is_empty() {
println!("\nDescription:\n{}", s.description);
}
println!("\nCode:\n```{}\n{}\n```", s.language, s.code);
} else {
println!("Snippet not found: {}", &id[..8]);
}
}
SnippetCommands::Rm { id } => {
let mut manager = SnippetManager::new()?;
match manager.remove(&id) {
Ok(s) => println!("✅ Deleted snippet: {}", s.title),
Err(_) => println!("Snippet not found: {}", &id[..8]),
}
}
SnippetCommands::Update { id, title, code, description } => {
let mut manager = SnippetManager::new()?;
match manager.update(&id, title.as_deref(), code.as_deref(), description.as_deref()) {
Ok(s) => println!("✅ Updated snippet: {}", s.title),
Err(e) => println!("Error: {}", e),
}
}
SnippetCommands::TagAdd { id, tag } => {
let mut manager = SnippetManager::new()?;
match manager.add_tag(&id, &tag) {
Ok(_) => println!("✅ Added tag '{}' to snippet", tag),
Err(e) => println!("Error: {}", e),
}
}
SnippetCommands::TagRm { id, tag } => {
let mut manager = SnippetManager::new()?;
match manager.remove_tag(&id, &tag) {
Ok(_) => println!("✅ Removed tag '{}' from snippet", tag),
Err(e) => println!("Error: {}", e),
}
}
SnippetCommands::Favorite { id } => {
let mut manager = SnippetManager::new()?;
match manager.toggle_favorite(&id) {
Ok(is_fav) => {
if is_fav {
println!("⭐ Added to favorites");
} else {
println!("Removed from favorites");
}
}
Err(e) => println!("Error: {}", e),
}
}
SnippetCommands::Stats => {
let manager = SnippetManager::new()?;
let stats = manager.stats();
println!("\n=== Snippet Statistics ===\n");
println!("Total snippets: {}", stats.total);
println!("Languages: {}", stats.languages);
println!("Tags: {}", stats.tags);
println!("Favorites: {}", stats.favorites);
}
SnippetCommands::Languages => {
let manager = SnippetManager::new()?;
let langs = manager.languages();
println!("\n=== Languages ===\n");
for lang in langs {
println!(" {}", lang);
}
}
SnippetCommands::Tags => {
let manager = SnippetManager::new()?;
let tags = manager.all_tags();
println!("\n=== Tags ===\n");
for tag in tags {
println!(" {}", tag);
}
}
}
Ok(())
}async fn handle_vuln_command(command: VulnCommands) -> Result<()> {
use crate::vuln::{Ecosystem, VulnScanner};
match command {
VulnCommands::Scan { path } => {
let project_path = path.map(PathBuf::from).unwrap_or_else(|| std::env::current_dir().unwrap());
println!("\n=== Vulnerability Scan ===\n");
println!("Scanning: {}", project_path.display());
let scanner = VulnScanner::new();
match scanner.scan_project(&project_path).await {
Ok(result) => {
println!("\n{}", result.summary());
println!("Dependencies scanned: {}", result.dependencies.len());
if result.has_vulnerabilities() {
println!("\n--- Vulnerabilities ---\n");
for vuln in &result.vulnerabilities {
let severity_str = match vuln.severity {
crate::vuln::Severity::Critical => "🔴 CRITICAL",
crate::vuln::Severity::High => "🟠 HIGH",
crate::vuln::Severity::Medium => "🟡 MEDIUM",
crate::vuln::Severity::Low => "🟢 LOW",
};
println!("{} {} - {}", severity_str, vuln.id, vuln.title);
println!(" Package: {} {}", vuln.package, vuln.patched_in.as_ref().map(|v| format!("(fixed in {})", v)).unwrap_or_default());
if !vuln.url.is_empty() {
println!(" URL: {}", vuln.url);
}
println!();
}
}
if !result.is_complete() {
eprintln!("\n--- Lookup failures ({}) ---", result.scan_errors.len());
for err in result.scan_errors.iter().take(10) {
eprintln!(" ! {}", err);
}
if result.scan_errors.len() > 10 {
eprintln!(" ... {} more", result.scan_errors.len() - 10);
}
std::process::exit(2);
}
}
Err(e) => {
println!("Scan failed: {}", e);
std::process::exit(2);
}
}
}
VulnCommands::Check { package, version, ecosystem } => {
println!("\n=== Quick Check ===\n");
println!("Package: {} v{}", package, version);
println!("Ecosystem: {}\n", ecosystem);
let eco = match ecosystem.as_str() {
"npm" => Ecosystem::Npm,
"cargo" => Ecosystem::Cargo,
"pip" => Ecosystem::Pip,
"go" => Ecosystem::Go,
_ => Ecosystem::Npm,
};
let scanner = VulnScanner::new();
match scanner.quick_check(&package, &version, eco).await {
Ok(vulns) => {
if vulns.is_empty() {
println!("✅ No vulnerabilities found");
} else {
println!("Found {} vulnerability(ies):\n", vulns.len());
for vuln in &vulns {
let severity_str = match vuln.severity {
crate::vuln::Severity::Critical => "🔴 CRITICAL",
crate::vuln::Severity::High => "🟠 HIGH",
crate::vuln::Severity::Medium => "🟡 MEDIUM",
crate::vuln::Severity::Low => "🟢 LOW",
};
println!("{} {}", severity_str, vuln.title);
println!(" ID: {}", vuln.id);
if !vuln.url.is_empty() {
println!(" URL: {}", vuln.url);
}
println!();
}
}
}
Err(e) => {
println!("Check failed: {}", e);
}
}
}
}
Ok(())
}
async fn handle_share_command(command: ShareCommands) -> Result<()> {
use crate::share::{
find_session, list_all_sessions, redact, registry,
render::{self, RenderFormat},
upload,
};
match command {
ShareCommands::Ls { provider, since, json } => {
let mut sessions = list_all_sessions();
if let Some(p) = &provider {
sessions.retain(|s| s.provider == *p);
}
if let Some(spec) = &since {
let dur = crate::share::parse_duration(spec)
.map_err(|e| anyhow::anyhow!(e))?;
let cutoff = chrono::Utc::now() - dur;
sessions.retain(|s| s.started_at.map(|t| t >= cutoff).unwrap_or(false));
}
if json {
use std::io::Write;
let mut out = std::io::stdout().lock();
for s in &sessions {
let line = serde_json::to_string(s).unwrap_or_default();
let _ = writeln!(out, "{}", line);
}
return Ok(());
}
let any_filter = provider.is_some() || since.is_some();
if sessions.is_empty() {
println!(
"No sessions found.{}",
if any_filter {
" (filter applied — try without --provider/--since)"
} else {
""
}
);
println!(
"\nSupported providers: {}",
registry()
.iter()
.map(|p| p.name())
.collect::<Vec<_>>()
.join(", ")
);
return Ok(());
}
println!("\n=== Agent sessions ({}) ===\n", sessions.len());
for s in sessions {
let when = s
.started_at
.map(|t| t.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_else(|| "?".to_string());
let project = s
.project_path
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "?".to_string());
let imported_tag = if s.imported { " ⤵️ imported" } else { "" };
println!(
"[{:<11}] {} ({} msgs){}",
s.provider, s.id, s.message_count, imported_tag
);
println!(" project: {}", project);
println!(" started: {}", when);
if let Some(t) = s.title_hint {
println!(" > {}", t);
}
println!();
}
}
ShareCommands::Export {
id,
format,
redact: do_redact,
output,
} => {
let format = RenderFormat::from_str_lossy(&format);
let session = find_session(&id).map_err(|e| anyhow::anyhow!(e))?;
let mut rendered = render::render(&session, format)?;
if do_redact {
rendered = redact::redact_secrets(&rendered);
}
let out_path = match output {
Some(p) => PathBuf::from(p),
None => {
let dir = dirs::home_dir()
.ok_or_else(|| anyhow::anyhow!("no home directory"))?
.join(".i-self")
.join("shared");
std::fs::create_dir_all(&dir)?;
dir.join(format!("{}-{}.{}", session.provider, session.id, format.extension()))
}
};
std::fs::write(&out_path, &rendered)?;
println!(
"✅ Exported {} message(s) → {}{}",
session.messages.len(),
out_path.display(),
if do_redact { " (redacted)" } else { "" }
);
}
ShareCommands::Upload {
id,
format,
redact: do_redact,
expires_in,
} => {
use crate::config::AppConfig;
use crate::sync::{SyncConfig, SyncProvider};
let format = RenderFormat::from_str_lossy(&format);
let session = find_session(&id).map_err(|e| anyhow::anyhow!(e))?;
let mut rendered = render::render(&session, format)?;
if do_redact {
rendered = redact::redact_secrets(&rendered);
}
let app_cfg = AppConfig::load().unwrap_or_default();
let cloud = app_cfg.cloud;
let bucket = std::env::var("ISELF_SYNC_BUCKET")
.ok()
.or(cloud.bucket)
.unwrap_or_default();
if bucket.is_empty() {
anyhow::bail!(
"no bucket configured. Set [cloud] bucket in ~/.i-self/config.toml \
or ISELF_SYNC_BUCKET=<name>. See `i-self sync status`."
);
}
let endpoint = std::env::var("ISELF_SYNC_ENDPOINT")
.ok()
.or(cloud.endpoint)
.filter(|s| !s.is_empty());
let region = std::env::var("ISELF_SYNC_REGION")
.ok()
.or(cloud.region)
.unwrap_or_else(|| "us-east-1".to_string());
let force_path_style = endpoint.is_some()
&& std::env::var("ISELF_SYNC_VHOST_STYLE").ok().as_deref() != Some("1");
let sync_cfg = SyncConfig {
enabled: true,
provider: SyncProvider::S3,
bucket,
endpoint,
region,
access_key_id: std::env::var("AWS_ACCESS_KEY_ID").ok().or(cloud.access_key),
secret_access_key: std::env::var("AWS_SECRET_ACCESS_KEY").ok().or(cloud.secret_key),
prefix: std::env::var("ISELF_SYNC_PREFIX").ok().or(cloud.prefix),
force_path_style,
auto_sync: false,
sync_interval_minutes: 60,
};
let prefix = sync_cfg.prefix.clone().unwrap_or_default();
let key = if prefix.is_empty() {
format!("shared/{}-{}.{}", session.provider, session.id, format.extension())
} else {
format!(
"{}/shared/{}-{}.{}",
prefix.trim_end_matches('/'),
session.provider,
session.id,
format.extension()
)
};
println!("Uploading to {}/{} ...", sync_cfg.bucket, key);
let result = upload::upload_with_presigned_url(
&sync_cfg,
&key,
rendered.into_bytes(),
format.content_type(),
std::time::Duration::from_secs(expires_in),
)
.await?;
println!("\n✅ Uploaded ({} byte session)", session.messages.len());
println!(" bucket: {}", result.bucket);
println!(" key: {}", result.key);
println!(" expires: in {}s", result.expires_in.as_secs());
println!("\n🔗 Presigned URL (treat as a credential — anyone with this can read until it expires):");
println!(" {}", result.presigned_url);
}
ShareCommands::Import {
input,
target,
project,
} => {
use crate::share::import_session::{find_importer, load_shared, ImportOptions};
let session = load_shared(&input).await.map_err(|e| anyhow::anyhow!(e))?;
let importer = find_importer(&target).ok_or_else(|| {
anyhow::anyhow!(
"unknown target `{}`. Supported: claude-code, aider, clipboard.",
target
)
})?;
let opts = ImportOptions {
project_path: project.as_deref().map(PathBuf::from),
dest_root_override: None,
};
let status = importer
.import(&session, &opts)
.map_err(|e| anyhow::anyhow!(e))?;
if importer.name() == "clipboard" {
eprintln!("\n{}", status);
} else {
println!("\n✅ {}", status);
}
}
}
Ok(())
}