dsh-ops 0.0.0-stage → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +435 -0
  5. package/README.en.md +126 -0
  6. package/README.md +115 -2
  7. package/README.zh.md +116 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/release-0.2.1.md +72 -0
  12. package/docs/schema-baseline.json +64 -0
  13. package/docs/schema-current.json +84 -0
  14. package/docs/schema-measurement.md +17 -0
  15. package/dsh-plugin.json +88 -0
  16. package/icon.svg +12 -0
  17. package/lib/binary.js +409 -0
  18. package/lib/config.js +198 -0
  19. package/lib/handshake.js +252 -0
  20. package/lib/index.js +108 -0
  21. package/lib/jobs.js +42 -0
  22. package/lib/policy.js +64 -0
  23. package/lib/presentation.js +63 -0
  24. package/lib/profile-install.js +61 -0
  25. package/lib/rust.js +194 -0
  26. package/lib/session-shells.js +78 -0
  27. package/lib/shells.js +998 -0
  28. package/lib/tools.js +657 -0
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +114 -4
  32. package/vendor/fastctx/Cargo.lock +3210 -0
  33. package/vendor/fastctx/Cargo.toml +94 -0
  34. package/vendor/fastctx/FORK.md +119 -0
  35. package/vendor/fastctx/LICENSE-APACHE +201 -0
  36. package/vendor/fastctx/NOTICE +40 -0
  37. package/vendor/fastctx/README.md +439 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  39. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  40. package/vendor/fastctx/UPSTREAM.md +49 -0
  41. package/vendor/fastctx/build.rs +413 -0
  42. package/vendor/fastctx/src/background_status.rs +403 -0
  43. package/vendor/fastctx/src/binary.rs +75 -0
  44. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  45. package/vendor/fastctx/src/budget.rs +781 -0
  46. package/vendor/fastctx/src/cli/mod.rs +110 -0
  47. package/vendor/fastctx/src/context_guard.rs +289 -0
  48. package/vendor/fastctx/src/control/mod.rs +6 -0
  49. package/vendor/fastctx/src/control/paths.rs +49 -0
  50. package/vendor/fastctx/src/control/settings.rs +753 -0
  51. package/vendor/fastctx/src/control/transaction.rs +531 -0
  52. package/vendor/fastctx/src/edit/document.rs +535 -0
  53. package/vendor/fastctx/src/edit/locks.rs +371 -0
  54. package/vendor/fastctx/src/edit/mod.rs +213 -0
  55. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  56. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  57. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  58. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  59. package/vendor/fastctx/src/edit_server.rs +53 -0
  60. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  61. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  62. package/vendor/fastctx/src/encoding.rs +1118 -0
  63. package/vendor/fastctx/src/file_executor.rs +1151 -0
  64. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  65. package/vendor/fastctx/src/glob_filter.rs +98 -0
  66. package/vendor/fastctx/src/glob_tool.rs +653 -0
  67. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  68. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  69. package/vendor/fastctx/src/lib.rs +45 -0
  70. package/vendor/fastctx/src/main.rs +15 -0
  71. package/vendor/fastctx/src/model.rs +51 -0
  72. package/vendor/fastctx/src/model_guidance.rs +62 -0
  73. package/vendor/fastctx/src/operation.rs +356 -0
  74. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  75. package/vendor/fastctx/src/os_environment.rs +414 -0
  76. package/vendor/fastctx/src/path_codec.rs +850 -0
  77. package/vendor/fastctx/src/paths.rs +244 -0
  78. package/vendor/fastctx/src/process_identity.rs +763 -0
  79. package/vendor/fastctx/src/process_policy.rs +74 -0
  80. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  81. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  82. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  83. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  84. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  85. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  86. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  87. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  88. package/vendor/fastctx/src/render_plan.rs +468 -0
  89. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  90. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  91. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  92. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  93. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  94. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  95. package/vendor/fastctx/src/runtime/session.rs +536 -0
  96. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  97. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  98. package/vendor/fastctx/src/search_text.rs +227 -0
  99. package/vendor/fastctx/src/server.rs +359 -0
  100. package/vendor/fastctx/src/server_manifest.rs +468 -0
  101. package/vendor/fastctx/src/server_support.rs +826 -0
  102. package/vendor/fastctx/src/session.rs +629 -0
  103. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  104. package/vendor/fastctx/src/shell/bash.rs +263 -0
  105. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  106. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  107. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  108. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  109. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  110. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  111. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  112. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  113. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  114. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  115. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  116. package/vendor/fastctx/src/shell/mod.rs +345 -0
  117. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  118. package/vendor/fastctx/src/shell/output.rs +406 -0
  119. package/vendor/fastctx/src/shell/process.rs +493 -0
  120. package/vendor/fastctx/src/shell_server.rs +156 -0
  121. package/vendor/fastctx/src/skip_report.rs +83 -0
  122. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  123. package/vendor/fastctx/src/tool_schema.rs +204 -0
  124. package/vendor/fastctx/src/traversal.rs +846 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  140. package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
@@ -0,0 +1,88 @@
1
+ //! Image magic-byte detection and MCP image-content construction for read.
2
+
3
+ use crate::model::{ToolContent, ToolResponse};
4
+ use crate::paths::io_error_message;
5
+ use base64::Engine;
6
+ use std::fs::File;
7
+ use std::io::Read;
8
+ use std::path::Path;
9
+
10
+ const MAX_IMAGE_BYTES: usize = 8 * 1024 * 1024;
11
+
12
+ pub(super) fn detect_image_mime(path: &Path, bytes: &[u8]) -> Option<&'static str> {
13
+ if bytes.starts_with(&[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]) {
14
+ return Some("image/png");
15
+ }
16
+ if bytes.starts_with(&[0xFF, 0xD8, 0xFF]) {
17
+ return Some("image/jpeg");
18
+ }
19
+ if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") {
20
+ return Some("image/gif");
21
+ }
22
+ if bytes.len() >= 12 && bytes.starts_with(b"RIFF") && &bytes[8..12] == b"WEBP" {
23
+ return Some("image/webp");
24
+ }
25
+ if bytes.starts_with(b"BM") {
26
+ return Some("image/bmp");
27
+ }
28
+
29
+ match path
30
+ .extension()
31
+ .and_then(|extension| extension.to_str())
32
+ .map(str::to_ascii_lowercase)
33
+ .as_deref()
34
+ {
35
+ Some("png") => Some("image/png"),
36
+ Some("jpg" | "jpeg") => Some("image/jpeg"),
37
+ Some("gif") => Some("image/gif"),
38
+ Some("webp") => Some("image/webp"),
39
+ Some("bmp") => Some("image/bmp"),
40
+ _ => None,
41
+ }
42
+ }
43
+
44
+ pub(super) fn read_image(path: &Path) -> ToolResponse {
45
+ let mut file = match File::open(path) {
46
+ Ok(file) => file,
47
+ Err(error) => return ToolResponse::error(io_error_message(path, &error)),
48
+ };
49
+ let reported_size = match file.metadata() {
50
+ Ok(metadata) => metadata.len(),
51
+ Err(error) => return ToolResponse::error(io_error_message(path, &error)),
52
+ };
53
+ if reported_size > MAX_IMAGE_BYTES as u64 {
54
+ return oversized_image(reported_size);
55
+ }
56
+ let mut bytes = Vec::with_capacity(reported_size as usize);
57
+ if let Err(error) = file
58
+ .by_ref()
59
+ .take(MAX_IMAGE_BYTES as u64 + 1)
60
+ .read_to_end(&mut bytes)
61
+ {
62
+ return ToolResponse::error(io_error_message(path, &error));
63
+ }
64
+ if bytes.len() > MAX_IMAGE_BYTES {
65
+ return oversized_image(bytes.len() as u64);
66
+ }
67
+ let Some(mime_type) = detect_image_mime(path, &bytes) else {
68
+ return ToolResponse::error(format!(
69
+ "Cannot read image file: {}. Retry after confirming it is a PNG, JPG, GIF, WebP, or BMP file.",
70
+ crate::paths::display_path(path)
71
+ ));
72
+ };
73
+ ToolResponse {
74
+ content: vec![ToolContent::Image {
75
+ data: base64::engine::general_purpose::STANDARD.encode(bytes),
76
+ mime_type: mime_type.to_string(),
77
+ detail: None,
78
+ }],
79
+ is_error: false,
80
+ }
81
+ }
82
+
83
+ fn oversized_image(size: u64) -> ToolResponse {
84
+ let mib = ((size as f64 / (1024.0 * 1024.0)) * 10.0).ceil() / 10.0;
85
+ ToolResponse::error(format!(
86
+ "Image file too large: {mib:.1} MiB (limit: 8 MiB). Resize or convert it externally."
87
+ ))
88
+ }
@@ -0,0 +1,245 @@
1
+ //! Text, image, PDF, and raw-byte dispatch for the file-inspection tool.
2
+
3
+ mod batch;
4
+ mod hex_file;
5
+ mod image_file;
6
+ #[cfg(feature = "pdf")]
7
+ mod pdf;
8
+ #[cfg(not(feature = "pdf"))]
9
+ #[path = "pdf_disabled.rs"]
10
+ mod pdf;
11
+ #[cfg(feature = "pdf")]
12
+ mod pdf_engine;
13
+ mod text_file;
14
+
15
+ use crate::binary::detect_binary_type;
16
+ use crate::budget::{READ_TOKEN_BUDGET_ENV, tool_token_budget};
17
+ use crate::model::ToolResponse;
18
+ use crate::paths::{
19
+ canonical_existing, display_path, io_error_message, missing_read_file_message,
20
+ parse_local_path_input,
21
+ };
22
+ use schemars::JsonSchema;
23
+ use serde::Deserialize;
24
+ use std::fs;
25
+ use std::io::Read;
26
+
27
+ /// Line window for a text read that omits `limit`: unbounded, so the token budget is the only
28
+ /// ceiling on how much one call returns.
29
+ ///
30
+ /// A second fixed window would silently cap every default read far below the configured budget,
31
+ /// forcing continuation calls the budget was raised to avoid. Do not reintroduce a numeric default
32
+ /// here; page explicitly with `limit` instead. (2026-07-25)
33
+ const UNBOUNDED_LINE_LIMIT: usize = usize::MAX;
34
+ /// Line window for a hex read that omits `limit`.
35
+ ///
36
+ /// Hex keeps a fixed default where text does not: a dump exists to inspect a byte range rather
37
+ /// than to deliver a whole binary, and its candidate window is estimated from the budget with a
38
+ /// deliberately loose factor, so an unbounded default would buffer hundreds of thousands of
39
+ /// rendered lines before the budget trimmed them back. (2026-07-25)
40
+ const DEFAULT_HEX_LINE_LIMIT: usize = 2_000;
41
+ const MAX_LINE_CHARS: usize = 2_000;
42
+ const TOTAL_COUNT_SIZE_LIMIT: u64 = 64 * 1024 * 1024;
43
+
44
+ /// Automatic channel dispatch or raw-byte viewing.
45
+ #[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema, Eq, PartialEq)]
46
+ #[serde(rename_all = "lowercase")]
47
+ enum ViewMode {
48
+ /// Select the text, image, or PDF channel from the file type.
49
+ #[default]
50
+ Auto,
51
+ /// Return a paged hexadecimal dump of the raw bytes.
52
+ Hex,
53
+ }
54
+
55
+ /// Parameters for the file-inspection tool; offset is a one-based line number.
56
+ #[derive(Clone, Debug, Deserialize, JsonSchema)]
57
+ #[serde(deny_unknown_fields)]
58
+ pub struct ReadRequest {
59
+ /// File to inspect; mutually exclusive with files.
60
+ #[schemars(description = crate::model_guidance::local_path_description(
61
+ "File to inspect; both / and \\\\ are accepted. Mutually exclusive with files."
62
+ ))]
63
+ pub file_path: Option<String>,
64
+ /// Batch form: an array of {"path", "offset"?, "limit"?, "encoding"?} objects for
65
+ /// 1-32 text ranges in one call. Repeat a path for distinct ranges, each with its own
66
+ /// offset/limit, and freely mix ranges from multiple files. Each entry behaves like its
67
+ /// own single-file text request; results stay in request order. Mutually exclusive with
68
+ /// file_path and with the top-level offset/encoding/pages/pdf_mode/view parameters.
69
+ #[schemars(length(min = 1, max = 32))]
70
+ pub files: Option<Vec<BatchReadEntry>>,
71
+ /// The 1-based line number to start reading from. Use for paging through large files.
72
+ #[schemars(range(min = 1))]
73
+ pub offset: Option<usize>,
74
+ /// The number of lines to return. With files, this is the default for entries that omit
75
+ /// limit; an entry's own limit takes precedence. Otherwise, omit it to let the output budget
76
+ /// decide.
77
+ #[schemars(range(min = 1))]
78
+ pub limit: Option<usize>,
79
+ /// Page range for PDF files, e.g. "1-5", "3", "10-20". Max 20 pages per call. Required in text mode for PDFs with more than 10 pages.
80
+ pub pages: Option<String>,
81
+ /// PDF only: "text" (default) returns the selected pages' text layer; "image" returns each selected page rendered as a PNG image.
82
+ #[schemars(with = "Option<pdf::PdfMode>")]
83
+ pub pdf_mode: Option<String>,
84
+ /// Text files only. Known source encoding as a WHATWG label, e.g. "gbk", "shift_jis", "big5", "euc-kr", "windows-1252", "utf-16le", plus "utf-32le"/"utf-32be". Selects how source bytes are decoded; output is always UTF-8. Omit for auto-detection; set it when you know the source encoding or the tool reports an ambiguous or undecodable encoding.
85
+ pub encoding: Option<String>,
86
+ /// "auto" (default) picks the channel by file type; "hex" returns a paged hex dump of the raw bytes of any file — the way to inspect binary files.
87
+ #[schemars(with = "Option<ViewMode>")]
88
+ pub view: Option<String>,
89
+ }
90
+
91
+ /// One text range in a batch inspection request.
92
+ #[derive(Clone, Debug, Deserialize, JsonSchema)]
93
+ #[serde(deny_unknown_fields)]
94
+ pub struct BatchReadEntry {
95
+ /// Text file to inspect.
96
+ #[schemars(description = crate::model_guidance::local_path_description(
97
+ "Text file to inspect; both / and \\\\ are accepted."
98
+ ))]
99
+ pub path: String,
100
+ /// The 1-based line number to start reading from.
101
+ #[schemars(range(min = 1))]
102
+ pub offset: Option<usize>,
103
+ /// Maximum lines to return from this range in this call. Overrides the top-level limit when
104
+ /// both are present; omit it to inherit that default or let the shared budget decide.
105
+ #[schemars(range(min = 1))]
106
+ pub limit: Option<usize>,
107
+ /// Known source encoding for this file, using the same labels as the top-level encoding parameter.
108
+ pub encoding: Option<String>,
109
+ }
110
+
111
+ /// Reads text, images, PDFs, or raw bytes and surfaces every expected failure explicitly.
112
+ pub fn read_file(request: ReadRequest) -> ToolResponse {
113
+ match (request.file_path.as_deref(), request.files.as_ref()) {
114
+ (Some(_), Some(_)) | (None, None) => {
115
+ return ToolResponse::error("Provide exactly one of file_path or files.");
116
+ }
117
+ (None, Some(_)) => return batch::read_text_files(request),
118
+ (Some(_), None) => {}
119
+ }
120
+ let file_path = request
121
+ .file_path
122
+ .as_deref()
123
+ .expect("single-file shape was validated");
124
+ let parsed = match parse_local_path_input(file_path) {
125
+ Ok(path) => path,
126
+ Err(message) => return ToolResponse::error(message),
127
+ };
128
+ let input_display = display_path(&parsed);
129
+ if !parsed.is_absolute() {
130
+ return ToolResponse::error(missing_read_file_message(&input_display));
131
+ }
132
+ let metadata = match fs::metadata(&parsed) {
133
+ Ok(metadata) => metadata,
134
+ Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
135
+ return ToolResponse::error(missing_read_file_message(&input_display));
136
+ }
137
+ Err(error) => return ToolResponse::error(io_error_message(&parsed, &error)),
138
+ };
139
+ let path = canonical_existing(&parsed).unwrap_or(parsed);
140
+ let path_display = display_path(&path);
141
+ if metadata.is_dir() {
142
+ return ToolResponse::error(format!(
143
+ "{path_display} is a directory, not a file. Use the glob tool to list its contents."
144
+ ));
145
+ }
146
+ if !metadata.is_file() {
147
+ return ToolResponse::error(format!(
148
+ "Cannot read non-regular file: {path_display}. Only regular files are supported."
149
+ ));
150
+ }
151
+ let mut prefix = Vec::new();
152
+ let prefix_result =
153
+ fs::File::open(&path).and_then(|file| file.take(8 * 1024).read_to_end(&mut prefix));
154
+ if let Err(error) = prefix_result {
155
+ return ToolResponse::error(io_error_message(&path, &error));
156
+ }
157
+
158
+ let view = match parse_view(request.view.as_deref()) {
159
+ Ok(view) => view,
160
+ Err(message) => return ToolResponse::error(message),
161
+ };
162
+ if view == ViewMode::Hex {
163
+ for (parameter, present) in [
164
+ ("pdf_mode", request.pdf_mode.is_some()),
165
+ ("pages", request.pages.is_some()),
166
+ ("encoding", request.encoding.is_some()),
167
+ ] {
168
+ if present {
169
+ return ToolResponse::error(format!(
170
+ "The {parameter} parameter cannot be combined with view=\"hex\"."
171
+ ));
172
+ }
173
+ }
174
+ let budget = match tool_token_budget(READ_TOKEN_BUDGET_ENV) {
175
+ Ok(budget) => budget,
176
+ Err(message) => return ToolResponse::error(message),
177
+ };
178
+ return hex_file::read_hex_file(&path, request.offset, request.limit, budget);
179
+ }
180
+
181
+ if pdf::is_pdf(&path, &prefix) {
182
+ if request.encoding.is_some() {
183
+ return ToolResponse::error("The encoding parameter only applies to text files.");
184
+ }
185
+ let mode = match pdf::parse_pdf_mode(request.pdf_mode.as_deref()) {
186
+ Ok(mode) => mode,
187
+ Err(message) => return ToolResponse::error(message),
188
+ };
189
+ let budget = if mode == pdf::PdfMode::Text {
190
+ match tool_token_budget(READ_TOKEN_BUDGET_ENV) {
191
+ Ok(budget) => Some(budget),
192
+ Err(message) => return ToolResponse::error(message),
193
+ }
194
+ } else {
195
+ None
196
+ };
197
+ return pdf::read_pdf(&path, request.pages.as_deref(), mode, budget);
198
+ }
199
+ if request.pages.is_some() {
200
+ return ToolResponse::error("The pages parameter only applies to PDF files.");
201
+ }
202
+ if request.pdf_mode.is_some() {
203
+ return ToolResponse::error("The pdf_mode parameter only applies to PDF files.");
204
+ }
205
+ if image_file::detect_image_mime(&path, &prefix).is_some() {
206
+ if request.encoding.is_some() {
207
+ return ToolResponse::error("The encoding parameter only applies to text files.");
208
+ }
209
+ return image_file::read_image(&path);
210
+ }
211
+ let budget = match tool_token_budget(READ_TOKEN_BUDGET_ENV) {
212
+ Ok(budget) => budget,
213
+ Err(message) => return ToolResponse::error(message),
214
+ };
215
+ text_file::read_text_file(
216
+ &path,
217
+ &path_display,
218
+ request.offset,
219
+ request.limit,
220
+ request.encoding.as_deref(),
221
+ detect_binary_type(&prefix),
222
+ budget,
223
+ )
224
+ }
225
+
226
+ fn parse_view(value: Option<&str>) -> Result<ViewMode, String> {
227
+ match value {
228
+ None | Some("auto") => Ok(ViewMode::Auto),
229
+ Some("hex") => Ok(ViewMode::Hex),
230
+ Some(value) => Err(format!(
231
+ "Invalid view value \"{value}\". Use \"auto\" or \"hex\"."
232
+ )),
233
+ }
234
+ }
235
+
236
+ fn binary_error(path_display: &str, binary_type: Option<&str>) -> ToolResponse {
237
+ ToolResponse::error(binary_error_message(path_display, binary_type))
238
+ }
239
+
240
+ fn binary_error_message(path_display: &str, binary_type: Option<&str>) -> String {
241
+ let kind = binary_type.map_or_else(String::new, |kind| format!(" (looks like {kind})"));
242
+ format!(
243
+ "Cannot read binary file as text: {path_display}{kind}. Use view=\"hex\" to inspect its raw bytes."
244
+ )
245
+ }