Skip to main content

relay_knowledge/interfaces/cli/
mod.rs

1//! CLI adapter for the shared application service.
2
3mod command;
4mod files;
5mod grammar;
6mod knowledge;
7pub(crate) mod map;
8mod operations;
9mod remote;
10mod render;
11mod repo;
12mod repo_set;
13mod runtime;
14mod service;
15mod setup;
16mod spec;
17mod version;
18
19use crate::{
20    api::ServicePlanRequest,
21    domain::{FreshnessPolicy, IndexKind, ProposalState, WorkerKind},
22};
23
24pub use command::{CliDiagnostic, CliError};
25use render::{render_response, serialize_line};
26pub(crate) use runtime::run_command;
27pub use runtime::run_with_service;
28
29/// Supported CLI output formats.
30#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
31pub enum OutputFormat {
32    #[default]
33    Text,
34    Json,
35    Markdown,
36    StreamingJson,
37}
38
39impl OutputFormat {
40    fn as_str(self) -> &'static str {
41        match self {
42            Self::Text => "text",
43            Self::Json => "json",
44            Self::Markdown => "markdown",
45            Self::StreamingJson => "streaming-json",
46        }
47    }
48
49    fn is_machine_readable(self) -> bool {
50        matches!(self, Self::Json | Self::StreamingJson)
51    }
52}
53
54/// Parsed CLI command.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CliCommand {
57    pub action: CliAction,
58    pub format: OutputFormat,
59    pub remote_base_url: Option<String>,
60    pub help: bool,
61}
62
63/// CLI action after global options are removed.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum CliAction {
66    Status,
67    Ingest {
68        source_scope: String,
69        content: String,
70        entity_labels: Vec<String>,
71    },
72    Query {
73        query: String,
74        source_scope: Option<String>,
75        limit: usize,
76        freshness: FreshnessPolicy,
77    },
78    FilesIndex {
79        source_scope: Option<String>,
80        roots: Vec<String>,
81    },
82    FilesQuery {
83        query: String,
84        source_scope: Option<String>,
85        root_id: Option<String>,
86        limit: usize,
87        freshness: FreshnessPolicy,
88    },
89    FilesContentQuery {
90        query: String,
91        source_scope: Option<String>,
92        root_id: Option<String>,
93        limit: usize,
94        freshness: FreshnessPolicy,
95    },
96    GraphInspect,
97    IndexRefresh {
98        kinds: Vec<IndexKind>,
99    },
100    Map(map::MapCommand),
101    WorkerStatus {
102        kind: Option<WorkerKind>,
103    },
104    WorkerRunOnce {
105        kind: Option<WorkerKind>,
106    },
107    ProposalList {
108        state: Option<ProposalState>,
109        limit: usize,
110    },
111    ProposalShow {
112        proposal_id: String,
113    },
114    ProposalAccept {
115        proposal_id: String,
116        actor: String,
117        reason: Option<String>,
118    },
119    ProposalReject {
120        proposal_id: String,
121        actor: String,
122        reason: Option<String>,
123    },
124    ProposalSupersede {
125        proposal_id: String,
126        actor: String,
127        reason: Option<String>,
128    },
129    AuditQuery {
130        operation: Option<String>,
131        limit: usize,
132    },
133    ProviderProbe,
134    Repo(repo::RepoCommand),
135    RepoSet(repo_set::RepoSetCommand),
136    Health,
137    ServiceStatus,
138    ServicePlan(ServicePlanRequest),
139    ServiceDefinitionWrite,
140    ServiceOperatorStatus,
141    ServiceOperatorPause,
142    ServiceOperatorResume,
143    ServiceWorkerRun {
144        task_id: Option<String>,
145    },
146    ServiceRun {
147        mcp: ServiceMcpTransport,
148        web: bool,
149    },
150    SetupDoctor,
151    SetupProfile {
152        profile: setup::SetupProfile,
153    },
154    Version,
155    VersionCheck,
156    Help {
157        path: Vec<String>,
158    },
159}
160
161/// MCP transport option for foreground service mode.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum ServiceMcpTransport {
164    Configured,
165    StreamableHttp,
166}
167
168/// Runs the CLI command and renders its response.
169pub async fn run<I, S>(args: I) -> Result<String, CliError>
170where
171    I: IntoIterator<Item = S>,
172    S: Into<String>,
173{
174    let output = crate::bootstrap::cli::run_process(args, false).await?;
175    Ok(output.stdout)
176}
177
178/// Rendered stdout/stderr for the process entry point.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct CliProcessOutput {
181    pub stdout: String,
182    pub stderr: String,
183}
184
185/// Runs the CLI command and renders only the command result.
186pub async fn run_process<I, S>(
187    args: I,
188    interactive_text_output: bool,
189) -> Result<CliProcessOutput, CliError>
190where
191    I: IntoIterator<Item = S>,
192    S: Into<String>,
193{
194    crate::bootstrap::cli::run_process(args, interactive_text_output).await
195}
196
197/// Renders best-effort process-only notices after primary command output is emitted.
198pub async fn process_update_notice<I, S>(args: I, interactive_text_output: bool) -> Option<String>
199where
200    I: IntoIterator<Item = S>,
201    S: Into<String>,
202{
203    let command = CliCommand::parse(args).ok()?;
204    version::update_notice_for_process(&command, interactive_text_output).await
205}
206
207#[cfg(test)]
208use service::ensure_web_remote_bind_allowed;
209
210#[cfg(test)]
211#[path = "tests/naming.rs"]
212mod cli_naming_tests;
213
214#[cfg(test)]
215#[path = "tests/general.rs"]
216mod cli_tests;
217
218#[cfg(test)]
219#[path = "tests/parse.rs"]
220mod cli_parse_tests;
221
222#[cfg(test)]
223#[path = "tests/remote.rs"]
224mod remote_cli_tests;
225
226#[cfg(test)]
227#[path = "tests/map.rs"]
228mod cli_map_tests;
229
230#[cfg(test)]
231#[path = "tests/service.rs"]
232mod cli_service_tests;
233
234#[cfg(test)]
235#[path = "tests/version.rs"]
236mod cli_version_tests;