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}