relay_knowledge/interfaces/cli/runtime/
dispatch.rs1use 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
93pub 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}