Skip to main content

saya_cli/
cli.rs

1use clap::{Args, Parser, Subcommand, ValueEnum};
2
3#[derive(Debug, Clone, Parser)]
4#[command(name = "saya", version, about = "Database-aware AI for the terminal")]
5pub struct Cli {
6    #[command(flatten)]
7    pub options: GlobalOptions,
8    #[command(subcommand)]
9    pub command: Option<Command>,
10}
11
12#[derive(Debug, Clone, Args, Default)]
13pub struct GlobalOptions {
14    #[arg(long = "continue", global = true)]
15    pub continue_session: bool,
16    #[arg(long, global = true)]
17    pub resume: Option<String>,
18    #[arg(long, global = true)]
19    pub profile: Option<String>,
20    #[arg(long = "include-profile", global = true)]
21    pub include_profiles: Vec<String>,
22    #[arg(long, value_name = "MODE", global = true)]
23    pub approval_mode: Option<String>,
24    #[arg(long, value_enum, default_value_t = FormatArg::Text, global = true)]
25    pub format: FormatArg,
26    #[arg(long, global = true)]
27    pub non_interactive: bool,
28    #[arg(long, global = true)]
29    pub config: Option<std::path::PathBuf>,
30    #[arg(long, global = true)]
31    pub connections: Option<std::path::PathBuf>,
32    #[arg(long, global = true)]
33    pub env_file: Option<std::path::PathBuf>,
34    #[arg(long, global = true)]
35    pub allow_data_sharing: bool,
36    #[arg(long, global = true)]
37    pub no_color: bool,
38    #[arg(long, short, global = true)]
39    pub verbose: bool,
40}
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
43pub enum FormatArg {
44    #[default]
45    Text,
46    Json,
47    Ndjson,
48}
49
50#[derive(Debug, Clone, Subcommand)]
51pub enum Command {
52    Config {
53        #[command(subcommand)]
54        command: ConfigCommand,
55    },
56    Connection {
57        #[command(subcommand)]
58        command: ConnectionCommand,
59    },
60    Ask {
61        prompt: Option<String>,
62        #[arg(long)]
63        file: Option<std::path::PathBuf>,
64    },
65    Query {
66        #[arg(long)]
67        sql: Option<String>,
68        #[arg(long)]
69        file: Option<std::path::PathBuf>,
70    },
71    Contracts {
72        #[command(subcommand)]
73        command: ContractsCommand,
74    },
75}
76
77#[derive(Debug, Clone, Subcommand)]
78pub enum ConfigCommand {
79    Init,
80    Doctor,
81    Show {
82        #[arg(long)]
83        resolved: bool,
84        #[arg(long)]
85        redacted: bool,
86    },
87}
88
89#[derive(Debug, Clone, Subcommand)]
90pub enum ConnectionCommand {
91    List,
92    Test {
93        #[arg(value_name = "PROFILE")]
94        profile_name: String,
95    },
96    Schema {
97        #[arg(value_name = "PROFILE")]
98        profile_name: String,
99        #[arg(long)]
100        refresh: bool,
101    },
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
105pub enum ContractsCommand {
106    List {
107        #[arg(long)]
108        profile: Option<String>,
109    },
110    Show {
111        table: String,
112        #[arg(long)]
113        profile: Option<String>,
114    },
115    Queue {
116        #[arg(long)]
117        profile: Option<String>,
118        /// Maximum candidates to list. Clamped to 200; a queue is a worklist,
119        /// not an archive.
120        #[arg(long)]
121        limit: Option<usize>,
122    },
123    Remember {
124        table: String,
125        #[arg(long, value_enum)]
126        kind: ClaimKindArg,
127        #[arg(long)]
128        value: String,
129        #[arg(long)]
130        column: Option<String>,
131        /// Why the directive claim holds — a sentence the model reads alongside
132        /// the value so a claim that contradicts a plausible schema reading
133        /// (use `return_date`, not `rental_date`) loses less often. Forwarded to
134        /// the directive kinds only (grain, time-column, column-role); ignored
135        /// for description/alias. Optional: a claim with no reason is the default.
136        #[arg(long)]
137        reason: Option<String>,
138        #[arg(long)]
139        profile: Option<String>,
140    },
141    Review {
142        claim_id: String,
143        #[arg(long)]
144        confirm: bool,
145        #[arg(long)]
146        reject: bool,
147    },
148    /// Act on a claim from the turn that just showed it, by a short stored
149    /// claim-id prefix (the `ki-xxxx` `contracts list` abbreviates to), not a
150    /// 64-character id. Spec D. The `prefix` is resolved against the resolved
151    /// profile's claims to exactly one claim, or refused; the decision then
152    /// reaches the existing `confirm`/`reject`/`use_candidate_once` operations
153    /// — it is not a second implementation of them.
154    Decide {
155        /// A leading prefix of a stored claim id. Unambiguous-or-refused: zero
156        /// matches or more than one is a typed error that changes nothing.
157        prefix: String,
158        #[arg(long, value_enum)]
159        decision: ReviewDecisionArg,
160        #[arg(long)]
161        profile: Option<String>,
162    },
163    Forget {
164        claim_id: String,
165        #[arg(long, value_enum, default_value_t = ForgetReasonArg::UserRequest)]
166        reason: ForgetReasonArg,
167    },
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
171pub enum ClaimKindArg {
172    Description,
173    Alias,
174    Grain,
175    ColumnDescription,
176    ColumnRole,
177    TimeColumn,
178}
179
180impl ClaimKindArg {
181    #[must_use]
182    pub const fn as_str(self) -> &'static str {
183        match self {
184            Self::Description => "description",
185            Self::Alias => "alias",
186            Self::Grain => "grain",
187            Self::ColumnDescription => "column-description",
188            Self::ColumnRole => "column-role",
189            Self::TimeColumn => "time-column",
190        }
191    }
192}
193
194#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
195pub enum ForgetReasonArg {
196    UserRequest,
197    Incorrect,
198    Obsolete,
199    Privacy,
200}
201
202/// The decision a `/confirm`, `/reject`, or `/use` short-reference command
203/// carries, resolved by `run_contracts` against the stored claim the prefix
204/// names. Spec D. `Confirm` and `Reject` reach the existing mutating ops; `UseOnce`
205/// reaches `use_candidate_once`, which validates and admits for one recall
206/// without promoting — a candidate stays a candidate.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
208pub enum ReviewDecisionArg {
209    Confirm,
210    Reject,
211    UseOnce,
212}