Skip to main content

spreadsheet_mcp/cli/
errors.rs

1use 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}