things3_cli/
lib.rs

1//! Things CLI library
2
3pub mod mcp;
4
5use clap::{Parser, Subcommand};
6use std::io::Write;
7use std::path::PathBuf;
8use things3_core::{Result, ThingsConfig, ThingsDatabase};
9
10#[derive(Parser, Debug)]
11#[command(name = "things3")]
12#[command(about = "Things 3 CLI with integrated MCP server")]
13#[command(version)]
14pub struct Cli {
15    /// Database path (defaults to Things 3 default location)
16    #[arg(long, short)]
17    pub database: Option<PathBuf>,
18
19    /// Fall back to default database path if specified path doesn't exist
20    #[arg(long)]
21    pub fallback_to_default: bool,
22
23    /// Verbose output
24    #[arg(long, short)]
25    pub verbose: bool,
26
27    #[command(subcommand)]
28    pub command: Commands,
29}
30
31#[derive(Subcommand, Debug, PartialEq)]
32pub enum Commands {
33    /// Get inbox tasks
34    Inbox {
35        /// Limit number of results
36        #[arg(long, short)]
37        limit: Option<usize>,
38    },
39    /// Get today's tasks
40    Today {
41        /// Limit number of results
42        #[arg(long, short)]
43        limit: Option<usize>,
44    },
45    /// Get projects
46    Projects {
47        /// Filter by area UUID
48        #[arg(long)]
49        area: Option<String>,
50        /// Limit number of results
51        #[arg(long, short)]
52        limit: Option<usize>,
53    },
54    /// Get areas
55    Areas {
56        /// Limit number of results
57        #[arg(long, short)]
58        limit: Option<usize>,
59    },
60    /// Search tasks
61    Search {
62        /// Search query
63        query: String,
64        /// Limit number of results
65        #[arg(long, short)]
66        limit: Option<usize>,
67    },
68    /// Start MCP server mode
69    Mcp,
70    /// Health check
71    Health,
72}
73
74pub fn print_tasks<W: Write>(
75    _db: &ThingsDatabase,
76    tasks: &[things3_core::Task],
77    writer: &mut W,
78) -> Result<()> {
79    if tasks.is_empty() {
80        writeln!(writer, "No tasks found")?;
81        return Ok(());
82    }
83
84    writeln!(writer, "Found {} tasks:", tasks.len())?;
85    for task in tasks {
86        writeln!(writer, "  • {} ({:?})", task.title, task.task_type)?;
87        if let Some(notes) = &task.notes {
88            writeln!(writer, "    Notes: {notes}")?;
89        }
90        if let Some(deadline) = &task.deadline {
91            writeln!(writer, "    Deadline: {deadline}")?;
92        }
93        if !task.tags.is_empty() {
94            writeln!(writer, "    Tags: {}", task.tags.join(", "))?;
95        }
96        writeln!(writer)?;
97    }
98    Ok(())
99}
100
101pub fn print_projects<W: Write>(
102    _db: &ThingsDatabase,
103    projects: &[things3_core::Project],
104    writer: &mut W,
105) -> Result<()> {
106    if projects.is_empty() {
107        writeln!(writer, "No projects found")?;
108        return Ok(());
109    }
110
111    writeln!(writer, "Found {} projects:", projects.len())?;
112    for project in projects {
113        writeln!(writer, "  • {} ({:?})", project.title, project.status)?;
114        if let Some(notes) = &project.notes {
115            writeln!(writer, "    Notes: {notes}")?;
116        }
117        if let Some(deadline) = &project.deadline {
118            writeln!(writer, "    Deadline: {deadline}")?;
119        }
120        if !project.tags.is_empty() {
121            writeln!(writer, "    Tags: {}", project.tags.join(", "))?;
122        }
123        writeln!(writer)?;
124    }
125    Ok(())
126}
127
128pub fn print_areas<W: Write>(
129    _db: &ThingsDatabase,
130    areas: &[things3_core::Area],
131    writer: &mut W,
132) -> Result<()> {
133    if areas.is_empty() {
134        writeln!(writer, "No areas found")?;
135        return Ok(());
136    }
137
138    writeln!(writer, "Found {} areas:", areas.len())?;
139    for area in areas {
140        writeln!(writer, "  • {}", area.title)?;
141        if let Some(notes) = &area.notes {
142            writeln!(writer, "    Notes: {notes}")?;
143        }
144        if !area.tags.is_empty() {
145            writeln!(writer, "    Tags: {}", area.tags.join(", "))?;
146        }
147        writeln!(writer)?;
148    }
149    Ok(())
150}
151
152pub fn health_check(db: &ThingsDatabase) -> Result<()> {
153    println!("🔍 Checking Things 3 database connection...");
154
155    // Try to get a small sample of tasks to verify connection
156    let tasks = db.get_inbox(Some(1))?;
157    println!("✅ Database connection successful!");
158    println!("   Found {} tasks in inbox", tasks.len());
159
160    // Try to get projects
161    let projects = db.get_projects(None)?;
162    println!("   Found {} projects", projects.len());
163
164    // Try to get areas
165    let areas = db.get_areas()?;
166    println!("   Found {} areas", areas.len());
167
168    println!("🎉 All systems operational!");
169    Ok(())
170}
171
172pub fn start_mcp_server(db: ThingsDatabase, config: ThingsConfig) -> Result<()> {
173    println!("🚀 Starting MCP server...");
174
175    let _server = mcp::ThingsMcpServer::new(db, config);
176
177    // In a real implementation, this would start the MCP server
178    // For now, we'll just print that it would start
179    println!("✅ MCP server would start here");
180    println!("   (This is a placeholder - actual MCP server implementation would go here)");
181
182    Ok(())
183}