1use crate::core::config::{self, try_team_salt};
5use crate::search::{SearchHit, SearchQuery};
6use crate::shell::cli::{open_workspace_read_store, workspace_path};
7use crate::shell::fmt::fmt_ts;
8use crate::store::Store;
9use anyhow::{Context, Result};
10use std::path::Path;
11
12pub fn cmd_sessions_search(
13 workspace: Option<&Path>,
14 query: &str,
15 since: Option<&str>,
16 agent: Option<&str>,
17 kind: Option<&str>,
18 limit: usize,
19) -> Result<()> {
20 print!(
21 "{}",
22 sessions_search_text(workspace, query, since, agent, kind, limit)?
23 );
24 Ok(())
25}
26
27pub fn sessions_search_text(
28 workspace: Option<&Path>,
29 query: &str,
30 since: Option<&str>,
31 agent: Option<&str>,
32 kind: Option<&str>,
33 limit: usize,
34) -> Result<String> {
35 let (hits, fallback) = sessions_search_hits(workspace, query, since, agent, kind, limit)?;
36 render_hits(&hits, fallback)
37}
38
39pub fn sessions_search_hits(
40 workspace: Option<&Path>,
41 query: &str,
42 since: Option<&str>,
43 agent: Option<&str>,
44 kind: Option<&str>,
45 limit: usize,
46) -> Result<(Vec<SearchHit>, bool)> {
47 let ws = workspace_path(workspace)?;
48 let store = open_workspace_read_store(&ws, false)?;
49 let cfg = config::load(&ws)?;
50 let salt = try_team_salt(&cfg.sync).unwrap_or([0; 32]);
51 let opts = SearchQuery {
52 query: query.to_string(),
53 since_ms: parse_since(since)?,
54 agent: agent.map(str::to_string),
55 kind: kind.map(str::to_string),
56 limit,
57 };
58 let data_dir = crate::core::paths::project_data_dir(&ws)?;
59 match crate::search::search(&data_dir, &opts, &ws, &salt, |s, q| store.get_event(s, q)) {
60 Ok(hits) => Ok((hits, false)),
61 Err(e) => anyhow::bail!("search index unavailable: {e}; run `kaizen search reindex`"),
62 }
63}
64
65pub fn cmd_search_reindex(workspace: Option<&Path>) -> Result<()> {
66 let ws = workspace_path(workspace)?;
67 let store = Store::open(&crate::core::workspace::db_path(&ws)?)?;
68 let cfg = config::load(&ws)?;
69 let ws_str = ws.to_string_lossy().to_string();
70 let sessions = store.list_sessions(&ws_str)?;
71 let events = store.workspace_events(&ws_str)?;
72 let data_dir = crate::core::paths::project_data_dir(&ws)?;
73 let stats = crate::search::reindex_workspace(&data_dir, &ws, &sessions, events, &cfg)
74 .context("reindex search")?;
75 println!(
76 "search reindex: {} events seen, {} docs indexed",
77 stats.events_seen, stats.docs_indexed
78 );
79 Ok(())
80}
81
82fn render_hits(hits: &[SearchHit], fallback: bool) -> Result<String> {
83 use std::fmt::Write;
84 let mut out = String::new();
85 if fallback {
86 writeln!(
87 out,
88 "warning: search index unavailable; falling back to event scan"
89 )?;
90 }
91 writeln!(out, "{:<40} {:<19} {:>7} SNIPPET", "SESSION", "TS", "SCORE")?;
92 for h in hits {
93 writeln!(
94 out,
95 "{:<40} {:<19} {:>7.3} {}",
96 h.session_id,
97 fmt_ts(h.ts_ms),
98 h.score,
99 h.snippet
100 )?;
101 }
102 Ok(out)
103}
104
105fn parse_since(raw: Option<&str>) -> Result<Option<u64>> {
106 let Some(raw) = raw else { return Ok(None) };
107 let days = raw.trim_end_matches('d').parse::<u64>()?;
108 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?;
109 Ok(Some(
110 (now.as_millis() as u64).saturating_sub(days.saturating_mul(86_400_000)),
111 ))
112}