@j0hanz/filesystem-mcp 1.19.1 → 2.1.0

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 (301) hide show
  1. package/README.md +333 -838
  2. package/dist/cli-help.d.ts +3 -0
  3. package/dist/cli-help.d.ts.map +1 -0
  4. package/dist/cli-help.js +169 -0
  5. package/dist/cli-help.js.map +1 -0
  6. package/dist/cli.d.ts +28 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +168 -123
  9. package/dist/cli.js.map +1 -0
  10. package/dist/core/concurrency.d.ts +70 -0
  11. package/dist/core/concurrency.d.ts.map +1 -0
  12. package/dist/core/concurrency.js +173 -0
  13. package/dist/core/concurrency.js.map +1 -0
  14. package/dist/core/config.d.ts +26 -0
  15. package/dist/core/config.d.ts.map +1 -0
  16. package/dist/core/config.js +18 -0
  17. package/dist/core/config.js.map +1 -0
  18. package/dist/core/cursor.d.ts +23 -0
  19. package/dist/core/cursor.d.ts.map +1 -0
  20. package/dist/core/cursor.js +62 -0
  21. package/dist/core/cursor.js.map +1 -0
  22. package/dist/core/errors.d.ts +96 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +356 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/file-uri.d.ts +24 -0
  27. package/dist/core/file-uri.d.ts.map +1 -0
  28. package/dist/core/file-uri.js +52 -0
  29. package/dist/core/file-uri.js.map +1 -0
  30. package/dist/core/fmt.d.ts +37 -0
  31. package/dist/core/fmt.d.ts.map +1 -0
  32. package/dist/core/fmt.js +93 -0
  33. package/dist/core/fmt.js.map +1 -0
  34. package/dist/core/fs.d.ts +108 -0
  35. package/dist/core/fs.d.ts.map +1 -0
  36. package/dist/core/fs.js +243 -0
  37. package/dist/core/fs.js.map +1 -0
  38. package/dist/core/glob.d.ts +41 -0
  39. package/dist/core/glob.d.ts.map +1 -0
  40. package/dist/core/glob.js +399 -0
  41. package/dist/core/glob.js.map +1 -0
  42. package/dist/core/input-required.d.ts +147 -0
  43. package/dist/core/input-required.d.ts.map +1 -0
  44. package/dist/core/input-required.js +261 -0
  45. package/dist/core/input-required.js.map +1 -0
  46. package/dist/core/mime.d.ts +15 -0
  47. package/dist/core/mime.d.ts.map +1 -0
  48. package/dist/core/mime.js +188 -0
  49. package/dist/core/mime.js.map +1 -0
  50. package/dist/core/observability.d.ts +10 -0
  51. package/dist/core/observability.d.ts.map +1 -0
  52. package/dist/core/observability.js +76 -0
  53. package/dist/core/observability.js.map +1 -0
  54. package/dist/core/page-store.d.ts +41 -0
  55. package/dist/core/page-store.d.ts.map +1 -0
  56. package/dist/core/page-store.js +74 -0
  57. package/dist/core/page-store.js.map +1 -0
  58. package/dist/core/path-completer.d.ts +10 -0
  59. package/dist/core/path-completer.d.ts.map +1 -0
  60. package/dist/core/path-completer.js +227 -0
  61. package/dist/core/path-completer.js.map +1 -0
  62. package/dist/core/path-discovery.d.ts +8 -0
  63. package/dist/core/path-discovery.d.ts.map +1 -0
  64. package/dist/core/path-discovery.js +105 -0
  65. package/dist/core/path-discovery.js.map +1 -0
  66. package/dist/core/path-utils.d.ts +9 -0
  67. package/dist/core/path-utils.d.ts.map +1 -0
  68. package/dist/core/path-utils.js +121 -0
  69. package/dist/core/path-utils.js.map +1 -0
  70. package/dist/core/path.d.ts +148 -0
  71. package/dist/core/path.d.ts.map +1 -0
  72. package/dist/core/path.js +674 -0
  73. package/dist/core/path.js.map +1 -0
  74. package/dist/core/primitives.d.ts +23 -0
  75. package/dist/core/primitives.d.ts.map +1 -0
  76. package/dist/core/primitives.js +54 -0
  77. package/dist/core/primitives.js.map +1 -0
  78. package/dist/core/read.d.ts +74 -0
  79. package/dist/core/read.d.ts.map +1 -0
  80. package/dist/core/read.js +562 -0
  81. package/dist/core/read.js.map +1 -0
  82. package/dist/core/schema.d.ts +139 -0
  83. package/dist/core/schema.d.ts.map +1 -0
  84. package/dist/core/schema.js +348 -0
  85. package/dist/core/schema.js.map +1 -0
  86. package/dist/core/search.d.ts +100 -0
  87. package/dist/core/search.d.ts.map +1 -0
  88. package/dist/core/search.js +248 -0
  89. package/dist/core/search.js.map +1 -0
  90. package/dist/core/sensitive.d.ts +6 -0
  91. package/dist/core/sensitive.d.ts.map +1 -0
  92. package/dist/core/sensitive.js +142 -0
  93. package/dist/core/sensitive.js.map +1 -0
  94. package/dist/core/store.d.ts +57 -0
  95. package/dist/core/store.d.ts.map +1 -0
  96. package/dist/core/store.js +162 -0
  97. package/dist/core/store.js.map +1 -0
  98. package/dist/core/util.d.ts +24 -0
  99. package/dist/core/util.d.ts.map +1 -0
  100. package/dist/core/util.js +65 -0
  101. package/dist/core/util.js.map +1 -0
  102. package/dist/core/watcher-registry.d.ts +68 -0
  103. package/dist/core/watcher-registry.d.ts.map +1 -0
  104. package/dist/core/watcher-registry.js +312 -0
  105. package/dist/core/watcher-registry.js.map +1 -0
  106. package/dist/http-policy.d.ts +121 -0
  107. package/dist/http-policy.d.ts.map +1 -0
  108. package/dist/http-policy.js +340 -0
  109. package/dist/http-policy.js.map +1 -0
  110. package/dist/index.d.ts +1 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +77 -41
  113. package/dist/index.js.map +1 -0
  114. package/dist/instructions.d.ts +11 -0
  115. package/dist/instructions.d.ts.map +1 -0
  116. package/dist/instructions.js +70 -0
  117. package/dist/instructions.js.map +1 -0
  118. package/dist/prompts.d.ts +8 -6
  119. package/dist/prompts.d.ts.map +1 -0
  120. package/dist/prompts.js +74 -168
  121. package/dist/prompts.js.map +1 -0
  122. package/dist/resources.d.ts +73 -9
  123. package/dist/resources.d.ts.map +1 -0
  124. package/dist/resources.js +392 -163
  125. package/dist/resources.js.map +1 -0
  126. package/dist/server.d.ts +62 -1
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +139 -1
  129. package/dist/server.js.map +1 -0
  130. package/dist/tools/batch.d.ts +89 -0
  131. package/dist/tools/batch.d.ts.map +1 -0
  132. package/dist/tools/batch.js +157 -0
  133. package/dist/tools/batch.js.map +1 -0
  134. package/dist/tools/create.d.ts +2 -0
  135. package/dist/tools/create.d.ts.map +1 -0
  136. package/dist/tools/create.js +124 -0
  137. package/dist/tools/create.js.map +1 -0
  138. package/dist/tools/define.d.ts +102 -0
  139. package/dist/tools/define.d.ts.map +1 -0
  140. package/dist/tools/define.js +404 -0
  141. package/dist/tools/define.js.map +1 -0
  142. package/dist/tools/delete-file.d.ts +2 -4
  143. package/dist/tools/delete-file.d.ts.map +1 -0
  144. package/dist/tools/delete-file.js +327 -87
  145. package/dist/tools/delete-file.js.map +1 -0
  146. package/dist/tools/diff.d.ts +2 -0
  147. package/dist/tools/diff.d.ts.map +1 -0
  148. package/dist/tools/diff.js +94 -0
  149. package/dist/tools/diff.js.map +1 -0
  150. package/dist/tools/edit.d.ts +2 -0
  151. package/dist/tools/edit.d.ts.map +1 -0
  152. package/dist/tools/edit.js +440 -0
  153. package/dist/tools/edit.js.map +1 -0
  154. package/dist/tools/index.d.ts +26 -0
  155. package/dist/tools/index.d.ts.map +1 -0
  156. package/dist/tools/index.js +52 -0
  157. package/dist/tools/index.js.map +1 -0
  158. package/dist/tools/list.d.ts +2 -0
  159. package/dist/tools/list.d.ts.map +1 -0
  160. package/dist/tools/list.js +301 -0
  161. package/dist/tools/list.js.map +1 -0
  162. package/dist/tools/move.d.ts +2 -0
  163. package/dist/tools/move.d.ts.map +1 -0
  164. package/dist/tools/move.js +321 -0
  165. package/dist/tools/move.js.map +1 -0
  166. package/dist/tools/patch.d.ts +2 -0
  167. package/dist/tools/patch.d.ts.map +1 -0
  168. package/dist/tools/patch.js +155 -0
  169. package/dist/tools/patch.js.map +1 -0
  170. package/dist/tools/progress.d.ts +52 -0
  171. package/dist/tools/progress.d.ts.map +1 -0
  172. package/dist/tools/progress.js +172 -0
  173. package/dist/tools/progress.js.map +1 -0
  174. package/dist/tools/read.d.ts +2 -4
  175. package/dist/tools/read.d.ts.map +1 -0
  176. package/dist/tools/read.js +423 -150
  177. package/dist/tools/read.js.map +1 -0
  178. package/dist/tools/replace-in-files.d.ts +2 -4
  179. package/dist/tools/replace-in-files.d.ts.map +1 -0
  180. package/dist/tools/replace-in-files.js +432 -264
  181. package/dist/tools/replace-in-files.js.map +1 -0
  182. package/dist/tools/roots.d.ts +2 -4
  183. package/dist/tools/roots.d.ts.map +1 -0
  184. package/dist/tools/roots.js +45 -52
  185. package/dist/tools/roots.js.map +1 -0
  186. package/dist/tools/search-content.d.ts +2 -4
  187. package/dist/tools/search-content.d.ts.map +1 -0
  188. package/dist/tools/search-content.js +262 -228
  189. package/dist/tools/search-content.js.map +1 -0
  190. package/dist/tools/search-files.d.ts +2 -4
  191. package/dist/tools/search-files.d.ts.map +1 -0
  192. package/dist/tools/search-files.js +162 -161
  193. package/dist/tools/search-files.js.map +1 -0
  194. package/dist/tools/stat.d.ts +2 -4
  195. package/dist/tools/stat.d.ts.map +1 -0
  196. package/dist/tools/stat.js +189 -62
  197. package/dist/tools/stat.js.map +1 -0
  198. package/dist/transport/http.d.ts +5 -0
  199. package/dist/transport/http.d.ts.map +1 -0
  200. package/dist/transport/http.js +276 -0
  201. package/dist/transport/http.js.map +1 -0
  202. package/dist/transport/shared.d.ts +45 -0
  203. package/dist/transport/shared.d.ts.map +1 -0
  204. package/dist/transport/shared.js +65 -0
  205. package/dist/transport/shared.js.map +1 -0
  206. package/dist/transport/stdio.d.ts +32 -0
  207. package/dist/transport/stdio.d.ts.map +1 -0
  208. package/dist/transport/stdio.js +279 -0
  209. package/dist/transport/stdio.js.map +1 -0
  210. package/dist/transport.d.ts +5 -0
  211. package/dist/transport.d.ts.map +1 -0
  212. package/dist/transport.js +4 -0
  213. package/dist/transport.js.map +1 -0
  214. package/package.json +37 -46
  215. package/dist/assets/logo.svg +0 -35
  216. package/dist/completions.d.ts +0 -2
  217. package/dist/completions.js +0 -572
  218. package/dist/config.d.ts +0 -119
  219. package/dist/config.js +0 -31
  220. package/dist/lib/abort.d.ts +0 -7
  221. package/dist/lib/abort.js +0 -81
  222. package/dist/lib/constants.d.ts +0 -42
  223. package/dist/lib/constants.js +0 -313
  224. package/dist/lib/errors.d.ts +0 -28
  225. package/dist/lib/errors.js +0 -301
  226. package/dist/lib/file-operations/core.d.ts +0 -53
  227. package/dist/lib/file-operations/core.js +0 -144
  228. package/dist/lib/file-operations/metadata.d.ts +0 -77
  229. package/dist/lib/file-operations/metadata.js +0 -878
  230. package/dist/lib/file-operations/search.d.ts +0 -46
  231. package/dist/lib/file-operations/search.js +0 -1194
  232. package/dist/lib/file-operations/traversal.d.ts +0 -40
  233. package/dist/lib/file-operations/traversal.js +0 -310
  234. package/dist/lib/fs-helpers.d.ts +0 -46
  235. package/dist/lib/fs-helpers.js +0 -606
  236. package/dist/lib/globs.d.ts +0 -2
  237. package/dist/lib/globs.js +0 -19
  238. package/dist/lib/logger.d.ts +0 -27
  239. package/dist/lib/logger.js +0 -91
  240. package/dist/lib/observability.d.ts +0 -33
  241. package/dist/lib/observability.js +0 -373
  242. package/dist/lib/paths.d.ts +0 -35
  243. package/dist/lib/paths.js +0 -634
  244. package/dist/lib/resource-store.d.ts +0 -28
  245. package/dist/lib/resource-store.js +0 -195
  246. package/dist/lib/utils.d.ts +0 -7
  247. package/dist/lib/utils.js +0 -37
  248. package/dist/lib/zod-codecs.d.ts +0 -2
  249. package/dist/lib/zod-codecs.js +0 -18
  250. package/dist/pkg-info.d.ts +0 -7
  251. package/dist/pkg-info.js +0 -9
  252. package/dist/resources/generated-instructions.d.ts +0 -1
  253. package/dist/resources/generated-instructions.js +0 -105
  254. package/dist/resources/tool-catalog.d.ts +0 -2
  255. package/dist/resources/tool-catalog.js +0 -78
  256. package/dist/resources/tool-info.d.ts +0 -9
  257. package/dist/resources/tool-info.js +0 -206
  258. package/dist/resources/workflows.d.ts +0 -1
  259. package/dist/resources/workflows.js +0 -72
  260. package/dist/schemas.d.ts +0 -505
  261. package/dist/schemas.js +0 -676
  262. package/dist/server/bootstrap.d.ts +0 -6
  263. package/dist/server/bootstrap.js +0 -585
  264. package/dist/server/event-store.d.ts +0 -18
  265. package/dist/server/event-store.js +0 -71
  266. package/dist/server/roots-manager.d.ts +0 -28
  267. package/dist/server/roots-manager.js +0 -227
  268. package/dist/server/task-store.d.ts +0 -10
  269. package/dist/server/task-store.js +0 -91
  270. package/dist/tools/apply-patch.d.ts +0 -4
  271. package/dist/tools/apply-patch.js +0 -235
  272. package/dist/tools/calculate-hash.d.ts +0 -4
  273. package/dist/tools/calculate-hash.js +0 -187
  274. package/dist/tools/contract.d.ts +0 -50
  275. package/dist/tools/contract.js +0 -1
  276. package/dist/tools/create-directory.d.ts +0 -4
  277. package/dist/tools/create-directory.js +0 -68
  278. package/dist/tools/diff-files.d.ts +0 -4
  279. package/dist/tools/diff-files.js +0 -136
  280. package/dist/tools/edit-file.d.ts +0 -4
  281. package/dist/tools/edit-file.js +0 -256
  282. package/dist/tools/icons.d.ts +0 -15
  283. package/dist/tools/icons.js +0 -24
  284. package/dist/tools/list-directory.d.ts +0 -4
  285. package/dist/tools/list-directory.js +0 -218
  286. package/dist/tools/move-file.d.ts +0 -4
  287. package/dist/tools/move-file.js +0 -178
  288. package/dist/tools/read-multiple.d.ts +0 -4
  289. package/dist/tools/read-multiple.js +0 -207
  290. package/dist/tools/shared.d.ts +0 -172
  291. package/dist/tools/shared.js +0 -599
  292. package/dist/tools/stat-many.d.ts +0 -4
  293. package/dist/tools/stat-many.js +0 -104
  294. package/dist/tools/task-support.d.ts +0 -29
  295. package/dist/tools/task-support.js +0 -473
  296. package/dist/tools/tree.d.ts +0 -4
  297. package/dist/tools/tree.js +0 -102
  298. package/dist/tools/write-file.d.ts +0 -4
  299. package/dist/tools/write-file.js +0 -60
  300. package/dist/tools.d.ts +0 -5
  301. package/dist/tools.js +0 -54
@@ -1,168 +1,441 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { basename } from 'node:path';
2
- import { DEFAULT_SEARCH_TIMEOUT_MS, MAX_TEXT_FILE_SIZE, } from '../lib/constants.js';
3
- import { ErrorCode } from '../lib/errors.js';
4
- import { calculateFileContentHash, readFile } from '../lib/fs-helpers.js';
5
- import { ReadFileInputSchema, ReadFileOutputSchema } from '../schemas.js';
6
- import { FILE_READ_ICONS } from './icons.js';
7
- import { buildResourceLink, buildToolErrorResponse, buildToolResponse, executeToolWithDiagnostics, maybeExternalizeTextContent, READ_ONLY_TOOL_ANNOTATIONS, } from './shared.js';
8
- import { registerStandardTool } from './task-support.js';
9
- export const READ_FILE_TOOL = {
10
- name: 'read',
11
- title: 'Read File',
12
- description: 'Read text file contents. ' +
13
- 'Use `head` to preview first N lines of large files. ' +
14
- 'For multiple files, use `read_many`.',
15
- inputSchema: ReadFileInputSchema,
16
- outputSchema: ReadFileOutputSchema,
17
- annotations: READ_ONLY_TOOL_ANNOTATIONS,
18
- icons: FILE_READ_ICONS,
19
- nuances: [
20
- 'Large content is externalized to `filesystem-mcp://result/{id}` and preview is returned inline.',
21
- ],
22
- taskSupport: 'forbidden',
23
- };
24
- const READ_TOOL_NAME = 'read';
25
- const READ_TOOL_LABEL = READ_FILE_TOOL.title;
26
- const FULL_FILE_CONTENTS_DESCRIPTION = 'Full file contents';
27
- function buildReadResourceName(filePath) {
28
- return `read:${basename(filePath)}`;
29
- }
30
- function buildReadOptions(args, signal) {
31
- const options = {
3
+ import * as z from 'zod/v4';
4
+ import { processInParallel } from '../core/concurrency.js';
5
+ import { ErrorCode } from '../core/errors.js';
6
+ import { buildFileResourceLinkFor, buildFileResourceUri } from '../core/file-uri.js';
7
+ import { detectMimeFromContent, detectMimeType } from '../core/mime.js';
8
+ import { readFileWithStats } from '../core/read.js';
9
+ import { ContinuationSchema, createReadRangeFields, defaultFalseBoolean, FileKind, NonNegInt, OperationSummarySchema, PerFileErrorSchema, PositiveInt, Sha256Hex, singleOrBatchAccessPaths, singleOrBatchPathsInput, validateReadRange, } from '../core/schema.js';
10
+ import { DEFAULT_CONTINUATION_CHUNK_SIZE, DEFAULT_SEARCH_TIMEOUT_MS, getDefaultReadManyMaxTotalSize, getMaxTextFileSize, PARALLEL_CONCURRENCY, } from '../core/util.js';
11
+ import { runOverPaths } from './batch.js';
12
+ import { defineTool } from './define.js';
13
+ const readRangeFields = createReadRangeFields({
14
+ head: 'Return first N lines',
15
+ tail: 'Return last N lines',
16
+ startLine: 'Start line (1-indexed)',
17
+ endLine: 'End line (1-indexed)',
18
+ });
19
+ const ReadFileInputSchema = singleOrBatchPathsInput({
20
+ extra: {
21
+ includeHash: defaultFalseBoolean('Include SHA-256 hash of the returned content in the response'),
22
+ ...readRangeFields,
23
+ },
24
+ })
25
+ .superRefine((value, ctx) => {
26
+ validateReadRange({
27
+ head: value.head,
28
+ tail: value.tail,
29
+ startLine: value.startLine,
30
+ endLine: value.endLine,
31
+ }, ctx);
32
+ })
33
+ // Mirror `validateReadRange` on the wire. Without this the four line params
34
+ // published as four independent optional integers, so `{ path, head, tail }`
35
+ // passed the advertised schema and was rejected only at call time.
36
+ //
37
+ // `oneOf` is restated because `.meta()` replaces the entry
38
+ // `singleOrBatchPathsInput` set rather than merging into it — dropping it here
39
+ // would silently un-publish the path/paths rule.
40
+ .meta({
41
+ // The line-param exclusivity that used to sit here as a five-branch
42
+ // `not/anyOf` is one sentence in the description instead: validateReadRange
43
+ // rejects every conflicting combination by name at runtime, so the wire copy
44
+ // was paying for a second statement of a rule the model reads better in
45
+ // prose.
46
+ oneOf: [{ required: ['path'] }, { required: ['paths'] }],
47
+ dependentRequired: { endLine: ['startLine'] },
48
+ });
49
+ // File bytes are NOT here: text rides the text content block, image/audio ride
50
+ // a media block. This schema is the metadata a caller cannot recover from
51
+ // those blocks. `readOnePath` carries the bytes internally via
52
+ // `PerPathReadValue` and `run` drops them before the structured half goes out.
53
+ const ReadPerPathValueSchema = z.strictObject({
54
+ mimeType: z.string().optional().describe('Detected MIME type (e.g. text/typescript)'),
55
+ kind: FileKind.optional().describe('Broad file kind: text, binary, image, audio, or pdf'),
56
+ resourceUri: z
57
+ .string()
58
+ .optional()
59
+ .describe('Resource URI for externalized content (present when file is stored in resource store)'),
60
+ continuation: ContinuationSchema.optional().describe('Next-read arguments; present when content was truncated due to size limits'),
61
+ totalLines: NonNegInt.optional().describe('Total line count in the full file'),
62
+ linesRead: NonNegInt.optional().describe('Number of lines returned in this response'),
63
+ hasMoreLines: z
64
+ .boolean()
65
+ .optional()
66
+ .describe('True when additional lines remain beyond what was returned'),
67
+ head: PositiveInt.optional().describe('Head lines requested'),
68
+ tail: PositiveInt.optional().describe('Tail lines requested'),
69
+ startLine: PositiveInt.optional().describe('Start line'),
70
+ endLine: PositiveInt.optional().describe('End line'),
71
+ contentHash: Sha256Hex.optional().describe('SHA-256 hex digest of the returned content (present when includeHash=true)'),
72
+ });
73
+ const ReadPerPathSchema = z.strictObject({
74
+ path: z.string().describe('Requested file path'),
75
+ value: ReadPerPathValueSchema.optional().describe('Read result; present on success'),
76
+ error: PerFileErrorSchema.optional().describe('Error details; present on failure'),
77
+ });
78
+ const ReadFileOutputSchema = z.strictObject({
79
+ results: z.array(ReadPerPathSchema).describe('Per-path results ordered to match the input paths'),
80
+ summary: OperationSummarySchema,
81
+ });
82
+ const READ_TOOL_LABEL = 'Read';
83
+ function buildReadSpec(args, signal) {
84
+ const common = {
32
85
  encoding: 'utf-8',
33
- maxSize: MAX_TEXT_FILE_SIZE,
86
+ maxSize: getMaxTextFileSize(),
34
87
  skipBinary: true,
88
+ ...(signal ? { signal } : {}),
35
89
  };
36
- if (signal)
37
- options.signal = signal;
38
- if (args.head !== undefined)
39
- options.head = args.head;
40
- if (args.tail !== undefined)
41
- options.tail = args.tail;
42
- if (args.startLine !== undefined)
43
- options.startLine = args.startLine;
44
- if (args.endLine !== undefined)
45
- options.endLine = args.endLine;
46
- return options;
47
- }
48
- function toStructuredReadFileResult(args, result) {
49
- const structured = {
50
- ok: true,
51
- path: args.path,
52
- content: result.content,
53
- };
54
- if (result.truncated)
55
- structured.truncated = true;
56
- if (result.totalLines !== undefined)
57
- structured.totalLines = result.totalLines;
58
- if (result.head !== undefined)
59
- structured.head = result.head;
60
- if (result.tail !== undefined)
61
- structured.tail = result.tail;
62
- if (result.startLine !== undefined)
63
- structured.startLine = result.startLine;
64
- if (result.endLine !== undefined)
65
- structured.endLine = result.endLine;
66
- if (result.linesRead !== undefined)
67
- structured.linesRead = result.linesRead;
68
- if (result.hasMoreLines)
69
- structured.hasMoreLines = true;
70
- return structured;
90
+ const { head, tail, startLine, endLine } = args;
91
+ if (head !== undefined)
92
+ return { kind: 'head', lines: head, ...common };
93
+ if (tail !== undefined)
94
+ return { kind: 'tail', lines: tail, ...common };
95
+ if (startLine !== undefined || endLine !== undefined) {
96
+ return {
97
+ kind: 'range',
98
+ start: startLine ?? 1,
99
+ ...(endLine !== undefined ? { end: endLine } : {}),
100
+ ...common,
101
+ };
102
+ }
103
+ return { kind: 'full', ...common };
71
104
  }
72
- function maybeBuildExternalizedReadResponse(filePath, content, structured, resourceStore) {
73
- const externalized = maybeExternalizeTextContent(resourceStore, content, {
74
- name: buildReadResourceName(filePath),
75
- mimeType: 'text/plain',
76
- });
77
- if (!externalized) {
105
+ function buildReadContinuation(result) {
106
+ if (!result.hasMoreLines)
78
107
  return undefined;
108
+ const linesRead = result.linesRead ?? 0;
109
+ const nextStart = (result.startLine ?? 1) + linesRead;
110
+ let chunkSize;
111
+ if (result.head !== undefined) {
112
+ chunkSize = result.head;
79
113
  }
80
- const { entry, preview } = externalized;
81
- const structuredWithResource = {
82
- ...structured,
83
- content: preview,
84
- truncated: true,
85
- resourceUri: entry.uri,
86
- };
87
- const text = [
88
- `Output too large to inline (${content.length} chars).`,
89
- 'Preview:',
90
- preview,
91
- ].join('\n');
92
- return buildToolResponse(text, structuredWithResource, [
93
- buildResourceLink({
94
- uri: entry.uri,
95
- name: entry.name,
96
- mimeType: entry.mimeType,
97
- description: FULL_FILE_CONTENTS_DESCRIPTION,
98
- expiresAt: entry.expiresAt,
99
- }),
100
- ]);
101
- }
102
- function buildReadProgressMessage(args) {
103
- const name = basename(args.path);
104
- if (args.startLine !== undefined) {
105
- const end = args.endLine ?? '…';
106
- return `${READ_TOOL_LABEL}: ${name} [lines ${args.startLine}–${end}]`;
114
+ else if (result.startLine !== undefined && result.endLine !== undefined) {
115
+ chunkSize = result.endLine - result.startLine + 1;
107
116
  }
108
- if (args.head !== undefined)
109
- return `${READ_TOOL_LABEL}: ${name} [head ${args.head}]`;
110
- if (args.tail !== undefined)
111
- return `${READ_TOOL_LABEL}: ${name} [tail ${args.tail}]`;
112
- return `${READ_TOOL_LABEL}: ${name}`;
113
- }
114
- function buildReadCompletionMessage(args, result) {
115
- const name = basename(args.path);
116
- if (result.isError)
117
- return `${READ_TOOL_LABEL}: ${name} • ${result.errorCode}`;
118
- const structured = result.structuredContent;
119
- const lines = structured.linesRead ?? structured.totalLines;
120
- if (structured.startLine !== undefined) {
121
- const end = structured.linesRead
122
- ? structured.startLine + structured.linesRead - 1
123
- : (structured.endLine ?? '…');
124
- return `${READ_TOOL_LABEL}: ${name} • lines ${structured.startLine}–${end}`;
117
+ else {
118
+ chunkSize = DEFAULT_CONTINUATION_CHUNK_SIZE;
125
119
  }
126
- if (structured.head !== undefined) {
127
- return structured.hasMoreLines
128
- ? `${READ_TOOL_LABEL}: ${name} first ${String(lines ?? structured.head)} lines`
129
- : `${READ_TOOL_LABEL}: ${name} ${String(lines ?? structured.head)} lines`;
120
+ const nextEnd = nextStart + chunkSize - 1;
121
+ const hint = result.totalLines
122
+ ? `${result.totalLines - nextStart + 1} lines remain (${nextStart}-${result.totalLines}). Read next chunk with these args.`
123
+ : 'File was truncated. Read next chunk with these args.';
124
+ return {
125
+ tool: 'read',
126
+ args: { path: result.path, startLine: nextStart, endLine: nextEnd },
127
+ hint,
128
+ };
129
+ }
130
+ async function collectFileBudget(filePaths, maxTotalSize, maxSize, ctx) {
131
+ const indexed = filePaths.map((path, index) => ({ path, index }));
132
+ const { results } = await processInParallel(indexed, async ({ path, index }) => {
133
+ try {
134
+ const out = await ctx.fs.stat(path);
135
+ return {
136
+ index,
137
+ size: Math.min(out.stats.size, maxSize),
138
+ validPath: out.validPath,
139
+ stats: out.stats,
140
+ };
141
+ }
142
+ catch (err) {
143
+ ctx.log?.('debug', `collectFileBudget: stat failed for "${path}": ${String(err)}`, 'read');
144
+ return undefined;
145
+ }
146
+ }, PARALLEL_CONCURRENCY, ctx.signal);
147
+ const byIndex = new Map();
148
+ const known = new Map();
149
+ for (const { value: item } of results) {
150
+ if (!item)
151
+ continue;
152
+ byIndex.set(item.index, item.size);
153
+ // Keyed by the ORIGINAL requested path string (not validPath) — that's
154
+ // what readOnePath is called with downstream, and what filePaths[i]
155
+ // holds.
156
+ const requestedPath = filePaths[item.index];
157
+ if (requestedPath !== undefined) {
158
+ known.set(requestedPath, { validPath: item.validPath, stats: item.stats });
159
+ }
130
160
  }
131
- if (structured.tail !== undefined) {
132
- return structured.hasMoreLines
133
- ? `${READ_TOOL_LABEL}: ${name} last ${String(lines ?? structured.tail)} lines`
134
- : `${READ_TOOL_LABEL}: ${name} • ${String(lines ?? structured.tail)} lines`;
161
+ let total = 0;
162
+ const skippedBudget = new Set();
163
+ for (let i = 0; i < filePaths.length; i += 1) {
164
+ const size = byIndex.get(i);
165
+ if (size === undefined)
166
+ continue;
167
+ if (total + size > maxTotalSize) {
168
+ // Only mark files that were actually stat'd as too large for the batch
169
+ // budget. A file whose stat already failed (byIndex undefined) must fall
170
+ // through to survivors so its read surfaces the real error (NOT_FILE /
171
+ // permission) — not a misleading TOO_LARGE.
172
+ for (let j = i; j < filePaths.length; j += 1) {
173
+ if (byIndex.get(j) === undefined)
174
+ continue;
175
+ skippedBudget.add(j);
176
+ }
177
+ break;
178
+ }
179
+ total += size;
135
180
  }
136
- if (structured.truncated) {
137
- return `${READ_TOOL_LABEL}: ${name} • truncated [${String(lines)} lines]`;
181
+ return { skippedBudget, known };
182
+ }
183
+ function preFilterByBudget(pathList, budgetState) {
184
+ const skippedResults = new Map();
185
+ const survivors = [];
186
+ for (let i = 0; i < pathList.length; i += 1) {
187
+ const path = pathList[i];
188
+ if (path === undefined)
189
+ continue;
190
+ if (budgetState.skippedBudget.has(i)) {
191
+ skippedResults.set(i, {
192
+ path,
193
+ error: {
194
+ code: ErrorCode.TOO_LARGE,
195
+ message: `Skipped: combined estimated read would exceed maxTotalSize (${String(budgetState.maxTotalSize)} bytes)`,
196
+ path,
197
+ },
198
+ });
199
+ continue;
200
+ }
201
+ survivors.push(path);
138
202
  }
139
- return `${READ_TOOL_LABEL}: ${name} ${String(lines)} lines`;
203
+ return { skippedResults, survivors };
204
+ }
205
+ function buildPerPathReadValue(result, options) {
206
+ const mimeInfo = detectMimeFromContent(result.path, result.content);
207
+ const continuation = result.hasMoreLines && result.readMode !== 'tail'
208
+ ? buildReadContinuation({
209
+ path: result.path,
210
+ hasMoreLines: true,
211
+ ...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
212
+ ...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
213
+ ...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
214
+ ...(result.head !== undefined ? { head: result.head } : {}),
215
+ ...(result.totalLines !== undefined ? { totalLines: result.totalLines } : {}),
216
+ })
217
+ : undefined;
218
+ const contentHash = options.includeHash
219
+ ? createHash('sha256').update(result.content, 'utf-8').digest('hex')
220
+ : undefined;
221
+ const resourceUri = options.hasResourceStore ? buildFileResourceUri(result.path) : undefined;
222
+ return {
223
+ content: result.content,
224
+ mimeType: mimeInfo.mimeType,
225
+ kind: mimeInfo.kind,
226
+ ...(continuation ? { continuation } : {}),
227
+ ...(result.totalLines !== undefined ? { totalLines: result.totalLines } : {}),
228
+ ...(result.linesRead !== undefined ? { linesRead: result.linesRead } : {}),
229
+ ...(result.hasMoreLines ? { hasMoreLines: true } : {}),
230
+ ...(result.head !== undefined ? { head: result.head } : {}),
231
+ ...(result.tail !== undefined ? { tail: result.tail } : {}),
232
+ ...(result.startLine !== undefined ? { startLine: result.startLine } : {}),
233
+ ...(result.endLine !== undefined ? { endLine: result.endLine } : {}),
234
+ ...(contentHash !== undefined ? { contentHash } : {}),
235
+ ...(resourceUri !== undefined ? { resourceUri } : {}),
236
+ };
140
237
  }
141
- async function handleReadFile(args, signal, resourceStore) {
142
- const options = buildReadOptions(args, signal);
143
- const result = await readFile(args.path, options);
144
- const structured = toStructuredReadFileResult(args, result);
145
- if (args.includeHash) {
146
- structured.contentHash = await calculateFileContentHash(result.path, signal);
238
+ /**
239
+ * The two facts the model cannot act on without: that lines were dropped, and
240
+ * the exact args to get the next chunk. Both used to live only in the structured
241
+ * half, which no longer reaches the model. Each line is `//` prefixed and set
242
+ * off by a blank line so it cannot be read as file bytes — do not drop either
243
+ * guard.
244
+ */
245
+ function readTrailer(v) {
246
+ const lines = [];
247
+ if (v.continuation) {
248
+ lines.push(`// truncated: ${v.continuation.hint} Continue: read ${JSON.stringify(v.continuation.args)}`);
147
249
  }
148
- const externalizedResponse = maybeBuildExternalizedReadResponse(args.path, result.content, structured, resourceStore);
149
- if (externalizedResponse) {
150
- return externalizedResponse;
250
+ else if (v.hasMoreLines && v.linesRead !== undefined) {
251
+ // A tail read gets no continuation — there are no args that read *backwards*
252
+ // — so say what was shown rather than leaving the truncation silent.
253
+ lines.push(`// truncated: showing last ${String(v.linesRead)} lines`);
151
254
  }
152
- return buildToolResponse(result.content, structured);
255
+ if (v.contentHash !== undefined)
256
+ lines.push(`// sha256: ${v.contentHash}`);
257
+ return lines;
153
258
  }
154
- export function registerReadFileTool(server, options = {}) {
155
- const handler = (args, ctx) => executeToolWithDiagnostics({
156
- toolName: READ_TOOL_NAME,
157
- ctx,
158
- outputSchema: ReadFileOutputSchema,
159
- timedSignal: { timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS },
160
- context: { path: args.path },
161
- run: (signal) => handleReadFile(args, signal, options.resourceStore),
162
- onError: (error) => buildToolErrorResponse(error, ErrorCode.NOT_FILE, args.path),
163
- });
164
- registerStandardTool(server, READ_FILE_TOOL, handler, options, {
165
- progressMessage: buildReadProgressMessage,
166
- completionMessage: buildReadCompletionMessage,
259
+ async function readOnePath(filePath, args, ctx, known) {
260
+ // Image/audio full read: return a media content block instead of throwing
261
+ // INVALID_INPUT ("Binary file detected."). Line-range reads are
262
+ // text-oriented and stay rejected. `readRaw` enforces the same size cap as
263
+ // the text path (getMaxTextFileSize), so a too-large image surfaces TOO_LARGE
264
+ // rather than blowing memory. svg carries kind:'image' but is XML text —
265
+ // readRaw.isBinary is false for it, so it falls through to the text path.
266
+ const isRangeRead = args.head !== undefined ||
267
+ args.tail !== undefined ||
268
+ args.startLine !== undefined ||
269
+ args.endLine !== undefined;
270
+ if (!isRangeRead) {
271
+ const mime = detectMimeType(filePath);
272
+ if (mime.kind === 'image' || mime.kind === 'audio') {
273
+ const raw = await ctx.fs.readRaw(filePath, { signal: ctx.signal });
274
+ if (raw.isBinary) {
275
+ return {
276
+ content: `[binary ${mime.kind} content: ${String(raw.content.length)} bytes; returned as a ${mime.kind} content block]`,
277
+ mimeType: raw.mimeType,
278
+ kind: mime.kind,
279
+ mediaData: raw.content.toString('base64'),
280
+ };
281
+ }
282
+ }
283
+ }
284
+ const spec = buildReadSpec(args, ctx.signal);
285
+ const result = known
286
+ ? await readFileWithStats(filePath, known.validPath, known.stats, spec)
287
+ : await ctx.fs.readFile(filePath, spec);
288
+ return buildPerPathReadValue(result, {
289
+ includeHash: args.includeHash,
290
+ hasResourceStore: ctx.resourceStore !== undefined,
167
291
  });
168
292
  }
293
+ export const READ_FILE = defineTool({
294
+ name: 'read',
295
+ title: 'Read File',
296
+ description: 'Read one or more text files and return content. ' +
297
+ 'Partial reads: head (first N lines), tail (last N lines), startLine/endLine (line range). ' +
298
+ 'Batch mode: pass paths[] instead of path; line params are shared across all files. ' +
299
+ 'head, tail, and startLine/endLine are mutually exclusive — use exactly one.',
300
+ input: ReadFileInputSchema,
301
+ output: ReadFileOutputSchema,
302
+ annotations: {
303
+ readOnlyHint: true,
304
+ idempotentHint: true,
305
+ destructiveHint: false,
306
+ openWorldHint: false,
307
+ },
308
+ timeoutMs: DEFAULT_SEARCH_TIMEOUT_MS,
309
+ defaultErrorCode: ErrorCode.NOT_FILE,
310
+ progress: (args) => {
311
+ const isBatch = args.paths !== undefined;
312
+ const name = isBatch ? `${String(args.paths?.length ?? 0)} files` : basename(args.path ?? '');
313
+ if (isBatch) {
314
+ return { label: READ_TOOL_LABEL, subject: name };
315
+ }
316
+ let scope;
317
+ if (args.startLine !== undefined) {
318
+ const end = args.endLine ?? '...';
319
+ scope = `${args.startLine}-${String(end)}`;
320
+ }
321
+ else if (args.head !== undefined) {
322
+ scope = `head ${args.head}`;
323
+ }
324
+ else if (args.tail !== undefined) {
325
+ scope = `tail ${args.tail}`;
326
+ }
327
+ return { label: READ_TOOL_LABEL, subject: name, ...(scope ? { scope } : {}) };
328
+ },
329
+ accessPaths: singleOrBatchAccessPaths,
330
+ run: async (args, ctx) => {
331
+ let pathList;
332
+ let skippedResults = new Map();
333
+ let survivors;
334
+ let known = new Map();
335
+ if (args.paths !== undefined) {
336
+ pathList = args.paths;
337
+ const defaultMaxTotalSize = getDefaultReadManyMaxTotalSize();
338
+ const maxTextFileSize = getMaxTextFileSize();
339
+ const budget = await collectFileBudget(pathList, defaultMaxTotalSize, maxTextFileSize, ctx);
340
+ known = budget.known;
341
+ const filtered = preFilterByBudget(pathList, {
342
+ skippedBudget: budget.skippedBudget,
343
+ maxTotalSize: defaultMaxTotalSize,
344
+ });
345
+ skippedResults = filtered.skippedResults;
346
+ survivors = filtered.survivors;
347
+ }
348
+ else {
349
+ pathList = [args.path ?? ''];
350
+ survivors = [...pathList];
351
+ }
352
+ const firstSurvivor = survivors[0];
353
+ const batchInput = firstSurvivor !== undefined && survivors.length === 1 && args.path !== undefined
354
+ ? { path: firstSurvivor }
355
+ : { paths: survivors };
356
+ // Every path can be budget-skipped (a single file over maxTotalSize does
357
+ // it), and runOverPaths rejects an empty list. The per-path TOO_LARGE
358
+ // results are already built — return those rather than failing the call.
359
+ const batch = survivors.length === 0
360
+ ? { results: [] }
361
+ : await runOverPaths(batchInput, ctx, ({ path }) => readOnePath(path, args, ctx, known.get(path)), { defaultErrorCode: ErrorCode.NOT_FILE });
362
+ const resultMap = new Map(batch.results.map((r) => [r.path, r]));
363
+ const ordered = pathList.map((path, idx) => {
364
+ const skipped = skippedResults.get(idx);
365
+ if (skipped)
366
+ return skipped;
367
+ const result = resultMap.get(path);
368
+ return (result ?? {
369
+ path,
370
+ error: { code: ErrorCode.UNKNOWN, message: 'Unknown read failure' },
371
+ });
372
+ });
373
+ const failed = ordered.filter((r) => 'error' in r).length;
374
+ const summary = {
375
+ total: ordered.length,
376
+ succeeded: ordered.length - failed,
377
+ failed,
378
+ };
379
+ const resources = [];
380
+ for (const result of ordered) {
381
+ if ('error' in result)
382
+ continue;
383
+ const v = result.value;
384
+ // Image/audio full read: emit the media content block alongside the
385
+ // structured value. The base64 lives in v.mediaData (set in readOnePath).
386
+ if (v.mediaData && v.mimeType && (v.kind === 'image' || v.kind === 'audio')) {
387
+ resources.push(v.kind === 'image'
388
+ ? { type: 'image', data: v.mediaData, mimeType: v.mimeType }
389
+ : { type: 'audio', data: v.mediaData, mimeType: v.mimeType });
390
+ continue;
391
+ }
392
+ if (!v.resourceUri || !v.content)
393
+ continue;
394
+ // `result.path` is the path as *requested*; `v.resourceUri` was built from
395
+ // the *validated* one. Rebuilding from the request would emit a second URI
396
+ // for the same file whenever the two differ in case — subscribe by one and
397
+ // unsubscribe by the other and the watcher never goes away.
398
+ resources.push(buildFileResourceLinkFor(v.resourceUri, basename(result.path), v.mimeType ?? 'application/octet-stream', Buffer.byteLength(v.content, 'utf8')));
399
+ }
400
+ const [firstOrdered] = ordered;
401
+ const body = (v) => {
402
+ const content = v.content ?? 'read failed';
403
+ const trailer = readTrailer(v);
404
+ if (trailer.length === 0)
405
+ return content;
406
+ // Exactly one blank line between the bytes and the trailer, whether or not
407
+ // the file ended in a newline.
408
+ return `${content}${content.endsWith('\n') ? '\n' : '\n\n'}${trailer.join('\n')}`;
409
+ };
410
+ const text = ordered.length === 1 && firstOrdered !== undefined
411
+ ? 'error' in firstOrdered
412
+ ? firstOrdered.error.message
413
+ : body(firstOrdered.value)
414
+ : ordered
415
+ .map((r) => {
416
+ const header = `// ${r.path}`;
417
+ if ('value' in r)
418
+ return `${header}\n${body(r.value)}`;
419
+ return `${header}\n// Error: ${r.error.message}`;
420
+ })
421
+ .join('\n\n');
422
+ // The bytes ship once. `text` above already carries every file's content
423
+ // verbatim (and image/audio bytes ride the media content block), so
424
+ // repeating them under `results[].value` doubled both the wire payload and
425
+ // the model's token cost for every read. The structured half keeps the
426
+ // metadata a client cannot recover from the text: paths, line counts,
427
+ // hashes, continuations, resourceUri.
428
+ const structuredResults = ordered.map((result) => {
429
+ if ('error' in result)
430
+ return result;
431
+ const { content: _content, mediaData: _mediaData, ...value } = result.value;
432
+ return { ...result, value };
433
+ });
434
+ return {
435
+ structured: { results: structuredResults, summary },
436
+ text,
437
+ ...(resources.length > 0 ? { resources } : {}),
438
+ };
439
+ },
440
+ });
441
+ //# sourceMappingURL=read.js.map