Skip to main content

relay_knowledge/interfaces/cli/runtime/
dispatch.rs

1use crate::{
2    api::{
3        GraphInspectionRequest, HybridRetrievalRequest, IndexRefreshRequest, IngestEvidence,
4        IngestRequest, InterfaceKind, RequestContext,
5    },
6    application::RelayKnowledgeService,
7    env::{EnvironmentConfig, RemoteCliEnvironmentConfig},
8};
9
10use super::{
11    super::{
12        CliAction, CliCommand, CliError, files, map, operations, remote,
13        render::{render_project_status, render_response},
14        repo, repo_set, service, setup, spec, version,
15    },
16    selection::{remote_environment_needed, select_remote_base_url},
17};
18
19#[cfg(test)]
20#[path = "dispatch_tests.rs"]
21mod tests;
22
23pub(crate) async fn run_command(command: CliCommand) -> Result<String, CliError> {
24    if let CliAction::Help { path } = &command.action {
25        return spec::render_help(path, command.format);
26    }
27    if command.action == CliAction::Version {
28        return version::render_version(command.format);
29    }
30    if let CliAction::ServiceRun { mcp, web } = command.action.clone() {
31        return service::run_service(mcp, web).await;
32    }
33    if let CliAction::Map(map_command) = command.action.clone() {
34        let context = RequestContext::for_interface(InterfaceKind::Cli);
35        return map::run_map(map_command, None, context, command.format).await;
36    }
37
38    let context = RequestContext::for_interface(InterfaceKind::Cli);
39    if remote_environment_needed(&command) {
40        let remote_environment = RemoteCliEnvironmentConfig::from_process()
41            .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
42        if let Some(base_url) =
43            select_remote_base_url(&command, remote_environment.remote_cli.base_url)
44        {
45            let remote_output = remote::run_remote(
46                &remote_environment.network,
47                &base_url,
48                &command.action,
49                context.clone(),
50                command.format,
51            )
52            .await?;
53            if let Some(output) = remote_output {
54                return Ok(output);
55            }
56            return Err(remote_unsupported_error());
57        }
58    }
59
60    let environment = EnvironmentConfig::from_process()
61        .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
62    if let Some(base_url) =
63        select_remote_base_url(&command, environment.remote_cli.base_url.clone())
64    {
65        let remote_output = remote::run_remote(
66            &environment.network,
67            &base_url,
68            &command.action,
69            context.clone(),
70            command.format,
71        )
72        .await?;
73        if let Some(output) = remote_output {
74            return Ok(output);
75        }
76        return Err(remote_unsupported_error());
77    }
78
79    let service = RelayKnowledgeService::from_environment(&environment)
80        .await
81        .map_err(|error| CliError::RuntimeConfigFailed(error.to_string()))?;
82
83    run_with_service(&service, command, context).await
84}
85
86fn remote_unsupported_error() -> CliError {
87    CliError::ApiFailed(
88        "remote CLI mode supports repo list, repo index, repo scope preview, repo status, repo query, repo context, repo feature-flags, repo impact, repo report, repo software, and repo view"
89            .to_owned(),
90    )
91}
92
93/// Runs a parsed CLI command with an already composed service.
94pub async fn run_with_service(
95    service: &RelayKnowledgeService,
96    command: CliCommand,
97    context: RequestContext,
98) -> Result<String, CliError> {
99    let format = command.format;
100    if let Some(output) =
101        operations::run_operational_action(service, &command.action, context.clone(), format)
102            .await?
103    {
104        return Ok(output);
105    }
106    if let Some(output) =
107        setup::run_setup_action(service, &command.action, context.clone(), format)?
108    {
109        return Ok(output);
110    }
111    if let Some(output) =
112        files::run_files(service, &command.action, context.clone(), format).await?
113    {
114        return Ok(output);
115    }
116    match command.action {
117        CliAction::Status => {
118            let response = service
119                .project_status(context)
120                .await
121                .map_err(|error| CliError::api_failed(error, format))?;
122
123            render_project_status(&response, format)
124        }
125        CliAction::Ingest {
126            source_scope,
127            content,
128            entity_labels,
129        } => {
130            let response = service
131                .ingest(
132                    IngestRequest {
133                        source_scope,
134                        evidence: vec![IngestEvidence {
135                            id: None,
136                            source_path: None,
137                            span: None,
138                            confidence: None,
139                            status: None,
140                            content,
141                            entity_labels,
142                            extraction: None,
143                        }],
144                        relations: Vec::new(),
145                        claims: Vec::new(),
146                        events: Vec::new(),
147                    },
148                    context,
149                )
150                .await
151                .map_err(|error| CliError::api_failed(error, format))?;
152
153            render_response(
154                "knowledge.ingest",
155                response.metadata.clone(),
156                &response,
157                format,
158            )
159        }
160        CliAction::Query {
161            query,
162            source_scope,
163            limit,
164            freshness,
165        } => {
166            let response = service
167                .retrieve_context(
168                    HybridRetrievalRequest {
169                        query,
170                        source_scope,
171                        limit,
172                        freshness,
173                    },
174                    context,
175                )
176                .await
177                .map_err(|error| CliError::api_failed(error, format))?;
178
179            render_response(
180                "knowledge.retrieve_context",
181                response.metadata.clone(),
182                &response,
183                format,
184            )
185        }
186        CliAction::GraphInspect => {
187            let response = service
188                .inspect_graph(GraphInspectionRequest { source_scope: None }, context)
189                .await
190                .map_err(|error| CliError::api_failed(error, format))?;
191
192            render_response(
193                "graph.inspect",
194                response.metadata.clone(),
195                &response,
196                format,
197            )
198        }
199        CliAction::IndexRefresh { kinds } => {
200            let response = service
201                .refresh_indexes(IndexRefreshRequest { kinds }, context)
202                .await
203                .map_err(|error| CliError::api_failed(error, format))?;
204
205            render_response(
206                "index.refresh",
207                response.metadata.clone(),
208                &response,
209                format,
210            )
211        }
212        CliAction::Map(command) => map::run_map(command, None, context, format).await,
213        CliAction::Repo(command) => repo::run_repo(service, command, context, format).await,
214        CliAction::RepoSet(command) => {
215            repo_set::run_repo_set(service, command, context, format).await
216        }
217        CliAction::Health => {
218            let response = service
219                .health(context)
220                .await
221                .map_err(|error| CliError::api_failed(error, format))?;
222
223            render_response(
224                "service.health",
225                response.metadata.clone(),
226                &response,
227                format,
228            )
229        }
230        CliAction::ProviderProbe => {
231            let response = service
232                .probe_embedding_provider(context)
233                .await
234                .map_err(|error| CliError::api_failed(error, format))?;
235
236            render_response(
237                "provider.embedding.probe",
238                response.metadata.clone(),
239                &response,
240                format,
241            )
242        }
243        CliAction::VersionCheck => version::run_version_check(service, format).await,
244        CliAction::ServiceRun { .. } => Err(CliError::ServiceRunFailed(
245            "service run requires process runtime".to_owned(),
246        )),
247        CliAction::Help { path } => spec::render_help(&path, format),
248        CliAction::WorkerStatus { .. }
249        | CliAction::FilesIndex { .. }
250        | CliAction::FilesQuery { .. }
251        | CliAction::FilesContentQuery { .. }
252        | CliAction::WorkerRunOnce { .. }
253        | CliAction::ProposalList { .. }
254        | CliAction::ProposalShow { .. }
255        | CliAction::ProposalAccept { .. }
256        | CliAction::ProposalReject { .. }
257        | CliAction::ProposalSupersede { .. }
258        | CliAction::AuditQuery { .. }
259        | CliAction::ServiceStatus
260        | CliAction::ServicePlan(_)
261        | CliAction::ServiceDefinitionWrite
262        | CliAction::ServiceOperatorStatus
263        | CliAction::ServiceOperatorPause
264        | CliAction::ServiceOperatorResume
265        | CliAction::ServiceWorkerRun { .. }
266        | CliAction::SetupDoctor
267        | CliAction::SetupProfile { .. } => Err(CliError::ApiFailed(
268            "operational command was not handled by the service adapter".to_owned(),
269        )),
270        CliAction::Version => version::render_version(command.format),
271    }
272}