spreadsheet_mcp/cli/
errors.rs1use crate::cli::OutputFormat;
2use anyhow::{Result, bail};
3use serde::Serialize;
4
5pub fn ensure_output_supported(format: OutputFormat) -> Result<()> {
6 match format {
7 OutputFormat::Json => Ok(()),
8 OutputFormat::Csv => {
9 bail!("csv output is not implemented yet for this CLI; use --format json")
10 }
11 }
12}
13
14#[derive(Debug, Serialize)]
15pub struct ErrorEnvelope {
16 pub code: String,
17 pub message: String,
18 #[serde(skip_serializing_if = "Option::is_none")]
19 pub did_you_mean: Option<String>,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub try_this: Option<String>,
22}
23
24pub fn envelope_for(error: &anyhow::Error) -> ErrorEnvelope {
25 let message = error.to_string();
26
27 if let Some((requested, suggested)) = parse_sheet_suggestion(&message) {
28 return ErrorEnvelope {
29 code: "SHEET_NOT_FOUND".to_string(),
30 message: format!("sheet '{}' was not found", requested),
31 did_you_mean: Some(suggested),
32 try_this: Some(
33 "run `agent-spreadsheet list-sheets <file>` to inspect valid names".to_string(),
34 ),
35 };
36 }
37
38 if message.contains("does not exist") {
39 return ErrorEnvelope {
40 code: "FILE_NOT_FOUND".to_string(),
41 message,
42 did_you_mean: None,
43 try_this: Some("check the workbook path and permissions".to_string()),
44 };
45 }
46
47 if message.contains("at least one range") {
48 return ErrorEnvelope {
49 code: "INVALID_ARGUMENT".to_string(),
50 message,
51 did_you_mean: None,
52 try_this: Some("pass one or more A1 ranges, for example: `A1:C10`".to_string()),
53 };
54 }
55
56 if message.contains("at least one edit") {
57 return ErrorEnvelope {
58 code: "INVALID_ARGUMENT".to_string(),
59 message,
60 did_you_mean: None,
61 try_this: Some("add one or more edits like `A1=42` or `B2==SUM(A1:A1)`".to_string()),
62 };
63 }
64
65 if message.contains("invalid shorthand edit") {
66 return ErrorEnvelope {
67 code: "INVALID_EDIT_SYNTAX".to_string(),
68 message,
69 did_you_mean: None,
70 try_this: Some(
71 "use `<cell>=<value>` for values or `<cell>==<formula>` for formulas".to_string(),
72 ),
73 };
74 }
75
76 if message.contains("csv output is not implemented") {
77 return ErrorEnvelope {
78 code: "OUTPUT_FORMAT_UNSUPPORTED".to_string(),
79 message,
80 did_you_mean: Some("json".to_string()),
81 try_this: Some("re-run with `--format json`".to_string()),
82 };
83 }
84
85 ErrorEnvelope {
86 code: "COMMAND_FAILED".to_string(),
87 message,
88 did_you_mean: None,
89 try_this: None,
90 }
91}
92
93fn parse_sheet_suggestion(message: &str) -> Option<(String, String)> {
94 let prefix = "sheet '";
95 let not_found = "' not found; did you mean '";
96 let suffix = "' ?";
97
98 let start = message.find(prefix)? + prefix.len();
99 let rest = &message[start..];
100 let mid = rest.find(not_found)?;
101 let requested = &rest[..mid];
102 let suggestion_start = start + mid + not_found.len();
103 let suggestion_rest = &message[suggestion_start..];
104 let suggestion_end = suggestion_rest.find(suffix)?;
105 let suggested = &suggestion_rest[..suggestion_end];
106 Some((requested.to_string(), suggested.to_string()))
107}