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