use clap::{Parser, ValueEnum};
use devsql::{engine::detect_tables, UnifiedEngine};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "devsql")]
#[command(
about = "Query your AI coding history to become a better prompter.\n\nJoin Claude/Codex conversations with Git commits to find your most productive prompts,\nidentify struggle sessions, and learn what actually works for you."
)]
#[command(version)]
#[command(after_help = r#"WHAT YOU CAN DISCOVER:
• Which prompts led to the most commits
• When you struggle (many messages, few commits)
• What tools Claude uses during productive sessions
• Which Codex prompts in jhistory correlate with commits
• Patterns in your successful coding sessions
EXAMPLE PROMPTS FOR YOUR AI AGENT:
"Use devsql to find my 10 most effective prompts from the past month"
"Query my history to find sessions where I struggled—many prompts, few commits"
"Analyze what my productive days have in common using devsql"
Learn more: https://github.com/douglance/devsql"#)]
struct Cli {
query: Option<String>,
#[arg(short = 'r', long = "repo", default_value = ".")]
repo: PathBuf,
#[arg(short = 'd', long = "data-dir")]
data_dir: Option<PathBuf>,
#[arg(short = 'f', long = "format", default_value = "table")]
format: OutputFormat,
#[arg(short = 'H', long = "no-header")]
no_header: bool,
}
#[derive(Clone, ValueEnum)]
enum OutputFormat {
Table,
Json,
Jsonl,
Csv,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let query = match cli.query {
Some(q) => q,
None => {
print_help();
return Ok(());
}
};
let claude_dir = cli
.data_dir
.unwrap_or_else(|| dirs::home_dir().unwrap().join(".claude"));
let repo_path = if cli.repo == PathBuf::from(".") {
std::env::current_dir()?
} else {
cli.repo.clone()
};
let mut engine = UnifiedEngine::new(claude_dir, repo_path)?;
let (claude_tables, git_tables) = detect_tables(&query);
let claude_refs: Vec<&str> = claude_tables.iter().map(|s| s.as_str()).collect();
let git_refs: Vec<&str> = git_tables.iter().map(|s| s.as_str()).collect();
engine.load_claude_tables(&claude_refs)?;
engine.load_git_tables(&git_refs)?;
let results = engine.query(&query)?;
match cli.format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&results)?);
}
OutputFormat::Jsonl => {
for row in &results {
println!("{}", serde_json::to_string(row)?);
}
}
OutputFormat::Csv => {
if results.is_empty() {
return Ok(());
}
let headers: Vec<&str> = results[0]
.as_object()
.map(|o| o.keys().map(|k| k.as_str()).collect())
.unwrap_or_default();
if !cli.no_header {
println!("{}", headers.join(","));
}
for row in &results {
if let Some(obj) = row.as_object() {
let values: Vec<String> = headers
.iter()
.map(|h| {
obj.get(*h)
.map(|v| match v {
serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
})
.unwrap_or_default()
})
.collect();
println!("{}", values.join(","));
}
}
}
OutputFormat::Table => {
print_table(&results, !cli.no_header);
}
}
Ok(())
}
fn print_table(results: &[serde_json::Value], show_header: bool) {
if results.is_empty() {
println!("No results");
return;
}
let headers: Vec<String> = results[0]
.as_object()
.map(|o| o.keys().cloned().collect())
.unwrap_or_default();
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in results {
if let Some(obj) = row.as_object() {
for (i, h) in headers.iter().enumerate() {
let val_len = obj
.get(h)
.map(|v| match v {
serde_json::Value::String(s) => s.len(),
other => other.to_string().len(),
})
.unwrap_or(0);
widths[i] = widths[i].max(val_len).min(50);
}
}
}
if show_header {
let header_line: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, h)| format!("{:width$}", h, width = widths[i]))
.collect();
println!("{}", header_line.join(" | "));
let separator: Vec<String> = widths.iter().map(|w| "-".repeat(*w)).collect();
println!("{}", separator.join("-+-"));
}
for row in results {
if let Some(obj) = row.as_object() {
let values: Vec<String> = headers
.iter()
.enumerate()
.map(|(i, h)| {
let val = obj
.get(h)
.map(|v| match v {
serde_json::Value::String(s) => s.clone(),
serde_json::Value::Null => String::new(),
other => other.to_string(),
})
.unwrap_or_default();
let truncated = if val.len() > widths[i] {
format!("{}...", &val[..widths[i].saturating_sub(3)])
} else {
val
};
format!("{:width$}", truncated, width = widths[i])
})
.collect();
println!("{}", values.join(" | "));
}
}
}
fn print_help() {
println!(
r#"devsql - Query your AI coding history to become a better prompter
Join Claude/Codex conversations with Git commits to find your most productive
prompts, identify struggle sessions, and learn what actually works for you.
USAGE:
devsql [OPTIONS] "SQL QUERY"
WHAT YOU CAN DISCOVER:
• Which prompts led to the most commits
• When you struggle (many messages, few commits)
• What tools Claude uses during productive sessions
• Patterns in your successful coding sessions
TABLES:
Claude Code: history (prompts), transcripts (conversations), todos
Codex CLI: jhistory / codex_history (session_id, ts, text, display, timestamp)
Git: commits, diffs, diff_files, branches
EXAMPLES:
Find your most productive prompts:
devsql "SELECT h.message, COUNT(c.id) as commits
FROM history h
LEFT JOIN commits c ON DATE(h.timestamp) = DATE(c.authored_at)
GROUP BY h.message HAVING commits > 0
ORDER BY commits DESC LIMIT 10"
Identify struggle days (many prompts, few commits):
devsql "SELECT DATE(h.timestamp) as day,
COUNT(*) as prompts, COUNT(DISTINCT c.id) as commits
FROM history h
LEFT JOIN commits c ON DATE(h.timestamp) = DATE(c.authored_at)
GROUP BY day ORDER BY prompts DESC LIMIT 10"
Recent Codex prompts:
devsql "SELECT datetime(timestamp/1000, 'unixepoch') as time, display
FROM jhistory ORDER BY timestamp DESC LIMIT 10"
OPTIONS:
-r, --repo PATH Git repository (default: current directory)
-d, --data-dir PATH Claude data (default: ~/.claude)
CODEX_HOME Codex data directory for jhistory (default: ~/.codex)
-f, --format FORMAT Output: table, json, jsonl, csv
-h, --help Show full help with more examples
TELL YOUR AI AGENT:
"Use devsql to find my most effective prompts from the past month"
"Query my history to find when I struggled most"
Learn more: https://github.com/douglance/devsql
"#
);
}