Skip to main content

chub_cli/commands/
context_cmd.rs

1use clap::Args;
2use owo_colors::OwoColorize;
3
4use chub_core::team::context;
5
6#[derive(Args)]
7pub struct ContextArgs {
8    /// Task description to find relevant context for
9    query: Option<String>,
10
11    /// List all project context docs
12    #[arg(long)]
13    list: bool,
14}
15
16pub fn run(args: ContextArgs, json: bool) {
17    if args.list || args.query.is_none() {
18        run_list(json);
19        return;
20    }
21
22    // Task-scoped context: show relevant docs for the given task
23    let query = args.query.as_deref().unwrap_or("");
24    let docs = context::discover_context_docs();
25
26    if docs.is_empty() {
27        if json {
28            println!("{}", serde_json::json!({"docs": [], "query": query}));
29        } else {
30            eprintln!(
31                "{}",
32                "No project context docs found in .chub/context/".dimmed()
33            );
34        }
35        return;
36    }
37
38    // Simple keyword matching for relevance
39    let query_lower = query.to_lowercase();
40    let query_words: Vec<&str> = query_lower.split_whitespace().collect();
41
42    let mut scored: Vec<(&context::ContextDoc, f64)> = docs
43        .iter()
44        .map(|doc| {
45            let mut score = 0.0f64;
46            let name_lower = doc.name.to_lowercase();
47            let desc_lower = doc.description.to_lowercase();
48
49            for word in &query_words {
50                if name_lower.contains(word) {
51                    score += 10.0;
52                }
53                if desc_lower.contains(word) {
54                    score += 5.0;
55                }
56                for tag in &doc.tags {
57                    if tag.to_lowercase().contains(word) {
58                        score += 8.0;
59                    }
60                }
61            }
62            (doc, score)
63        })
64        .collect();
65
66    scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
67
68    // Show all docs, ranked by relevance
69    if json {
70        let items: Vec<serde_json::Value> = scored
71            .iter()
72            .map(|(doc, score)| {
73                let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
74                serde_json::json!({
75                    "id": format!("project/{}", stem),
76                    "name": doc.name,
77                    "description": doc.description,
78                    "relevance": score,
79                })
80            })
81            .collect();
82        println!(
83            "{}",
84            serde_json::to_string_pretty(&serde_json::json!({
85                "query": query,
86                "docs": items,
87            }))
88            .unwrap_or_default()
89        );
90    } else {
91        eprintln!(
92            "Context for \"{}\": {} doc(s)\n",
93            query.bold(),
94            scored.len()
95        );
96        for (doc, score) in &scored {
97            let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
98            let relevance = if *score > 0.0 {
99                format!(" (relevance: {:.0})", score).dimmed().to_string()
100            } else {
101                String::new()
102            };
103            eprintln!(
104                "  {}  {}{}",
105                format!("project/{}", stem).cyan(),
106                doc.description.dimmed(),
107                relevance,
108            );
109        }
110    }
111}
112
113fn run_list(json: bool) {
114    let docs = context::list_context_docs();
115
116    if json {
117        let items: Vec<serde_json::Value> = docs
118            .iter()
119            .map(|doc| {
120                let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
121                serde_json::json!({
122                    "id": format!("project/{}", stem),
123                    "name": doc.name,
124                    "description": doc.description,
125                    "tags": doc.tags,
126                    "file": doc.file,
127                })
128            })
129            .collect();
130        println!(
131            "{}",
132            serde_json::to_string_pretty(&serde_json::json!({
133                "docs": items,
134                "total": docs.len(),
135            }))
136            .unwrap_or_default()
137        );
138    } else {
139        if docs.is_empty() {
140            eprintln!(
141                "{}",
142                "No project context docs. Add .md files to .chub/context/".dimmed()
143            );
144            return;
145        }
146        eprintln!(
147            "{}",
148            format!("{} project context docs:\n", docs.len()).bold()
149        );
150        for doc in &docs {
151            let stem = doc.file.strip_suffix(".md").unwrap_or(&doc.file);
152            eprintln!("  {}", format!("project/{}", stem).cyan());
153            if !doc.description.is_empty() {
154                eprintln!("    {}", doc.description.dimmed());
155            }
156        }
157    }
158}