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