Skip to main content

claudex_cli/
cli.rs

1//! Shared command-line filtering: the cross-cutting `--provider/--model/
2//! --since/--until/--on-disk-only` flags every reporting command accepts,
3//! resolved into a [`ResolvedFilter`] that the index queries (and the
4//! `--no-index` fallback) apply uniformly.
5
6use std::path::PathBuf;
7
8use anyhow::Result;
9use clap::{Args, Subcommand, ValueEnum};
10pub use claudex::filter::ResolvedFilter;
11use claudex::filter::{ProviderKind, parse_when};
12
13/// Provider selector accepted on the command line.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
15pub enum ProviderArg {
16    Claude,
17    Codex,
18    #[value(name = "openclaw")]
19    OpenClaw,
20    Pi,
21}
22
23impl From<ProviderArg> for ProviderKind {
24    fn from(provider: ProviderArg) -> Self {
25        match provider {
26            ProviderArg::Claude => ProviderKind::Claude,
27            ProviderArg::Codex => ProviderKind::Codex,
28            ProviderArg::OpenClaw => ProviderKind::OpenClaw,
29            ProviderArg::Pi => ProviderKind::Pi,
30        }
31    }
32}
33
34/// Cross-cutting filter flags shared by every reporting command. Flattened into
35/// each command alongside its own options (`--project`, `--limit`, …).
36#[derive(Args, Clone, Debug, Default)]
37pub struct FilterArgs {
38    /// Restrict to one or more providers (repeatable or comma-separated).
39    /// Default: all indexed providers.
40    #[arg(long, value_enum, value_delimiter = ',')]
41    pub provider: Vec<ProviderArg>,
42    /// Only sessions whose model matches this substring (e.g. `opus`, `gpt-5`).
43    #[arg(long)]
44    pub model: Option<String>,
45    /// Only sessions at/after this time — a date (`2026-01-01`), an RFC3339
46    /// timestamp, or a relative span (`7d`, `12h`, `2w`).
47    #[arg(long, value_parser = validate_when_arg)]
48    pub since: Option<String>,
49    /// Only sessions at/before this time (same formats as `--since`).
50    #[arg(long, value_parser = validate_when_arg)]
51    pub until: Option<String>,
52    /// Exclude sessions whose source file has been archived or deleted from
53    /// disk (retained in the index by default).
54    #[arg(long)]
55    pub on_disk_only: bool,
56}
57
58impl FilterArgs {
59    pub fn resolve(&self) -> Result<ResolvedFilter> {
60        let mut providers: Vec<String> = self
61            .provider
62            .iter()
63            .map(|p| ProviderKind::from(*p).id().to_string())
64            .collect();
65        providers.sort();
66        providers.dedup();
67        Ok(ResolvedFilter {
68            providers,
69            model: self.model.clone(),
70            since_ms: self
71                .since
72                .as_deref()
73                .map(|s| parse_when(s, false))
74                .transpose()?,
75            until_ms: self
76                .until
77                .as_deref()
78                .map(|s| parse_when(s, true))
79                .transpose()?,
80            on_disk_only: self.on_disk_only,
81        })
82    }
83}
84
85// --- `claudex skills` ---
86
87/// Generate or install the agent skill that describes claudex.
88#[derive(Subcommand, Debug)]
89pub enum SkillCommand {
90    /// Write skill files to a directory for review (default ./claudex-skills)
91    #[command(after_long_help = crate::cli_help::SKILLS_GENERATE_EXAMPLES)]
92    Generate(SkillArgs),
93    /// Write skill files into live harness configuration locations
94    #[command(after_long_help = crate::cli_help::SKILLS_INSTALL_EXAMPLES)]
95    Install(SkillArgs),
96}
97
98/// Options shared by `skills generate` and `skills install`.
99#[derive(Args, Debug, Clone)]
100pub struct SkillArgs {
101    /// Harness target(s) to write for (repeatable or comma-separated).
102    #[arg(long, value_enum, value_delimiter = ',', default_value = "all")]
103    pub target: Vec<SkillTarget>,
104    /// Output root (generate) or base directory override (install).
105    #[arg(long)]
106    pub dir: Option<PathBuf>,
107    /// Install to user-level config (~/) instead of the current project.
108    #[arg(long)]
109    pub global: bool,
110    /// Overwrite existing files.
111    #[arg(long)]
112    pub force: bool,
113    /// Output the summary as JSON.
114    #[arg(long)]
115    pub json: bool,
116}
117
118/// Harness flavor a skill is generated for.
119#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
120pub enum SkillTarget {
121    /// Claude Code — `.claude/skills/claudex/SKILL.md`
122    ClaudeCode,
123    /// OpenAI Codex — `.agents/skills/claudex/SKILL.md`
124    Codex,
125    /// Pi — `.pi/skills/claudex/SKILL.md`
126    Pi,
127    /// OpenClaw — `skills/claudex/SKILL.md` or `$OPENCLAW_STATE_DIR/skills/claudex/SKILL.md`
128    #[value(name = "openclaw")]
129    OpenClaw,
130    /// Idempotent block spliced into `AGENTS.md`
131    AgentsMd,
132    /// Claude Code plugin — `.claude-plugin/plugin.json` + skill
133    Plugin,
134    /// Expand to claude-code + codex + pi + openclaw + agents-md
135    All,
136}
137
138pub fn validate_when_arg(value: &str) -> std::result::Result<String, String> {
139    parse_when(value, false)
140        .map(|_| value.to_string())
141        .map_err(|e| e.to_string())
142}