@namzu/sdk 57.1.0 → 58.0.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 (240) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +2 -0
  4. package/dist/agents/QueryAgent.js.map +1 -1
  5. package/dist/agents/file-definitions.d.ts +35 -4
  6. package/dist/agents/file-definitions.d.ts.map +1 -1
  7. package/dist/agents/file-definitions.js +98 -8
  8. package/dist/agents/file-definitions.js.map +1 -1
  9. package/dist/authorization/protected-paths.d.ts +9 -0
  10. package/dist/authorization/protected-paths.d.ts.map +1 -1
  11. package/dist/authorization/protected-paths.js +29 -3
  12. package/dist/authorization/protected-paths.js.map +1 -1
  13. package/dist/connector/mcp/client.d.ts +8 -0
  14. package/dist/connector/mcp/client.d.ts.map +1 -1
  15. package/dist/connector/mcp/client.js +75 -2
  16. package/dist/connector/mcp/client.js.map +1 -1
  17. package/dist/constants/mcp/index.d.ts +2 -0
  18. package/dist/constants/mcp/index.d.ts.map +1 -1
  19. package/dist/constants/mcp/index.js +2 -0
  20. package/dist/constants/mcp/index.js.map +1 -1
  21. package/dist/constants/plugin/index.d.ts +2 -0
  22. package/dist/constants/plugin/index.d.ts.map +1 -1
  23. package/dist/constants/plugin/index.js +2 -0
  24. package/dist/constants/plugin/index.js.map +1 -1
  25. package/dist/manager/agent/lifecycle.d.ts +1 -1
  26. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  27. package/dist/manager/agent/lifecycle.js +25 -4
  28. package/dist/manager/agent/lifecycle.js.map +1 -1
  29. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  30. package/dist/manager/session/turn-recorder.js +2 -1
  31. package/dist/manager/session/turn-recorder.js.map +1 -1
  32. package/dist/persona/assembler.d.ts.map +1 -1
  33. package/dist/persona/assembler.js +2 -2
  34. package/dist/persona/assembler.js.map +1 -1
  35. package/dist/plugin/index.d.ts +1 -1
  36. package/dist/plugin/index.d.ts.map +1 -1
  37. package/dist/plugin/lifecycle.d.ts +8 -1
  38. package/dist/plugin/lifecycle.d.ts.map +1 -1
  39. package/dist/plugin/lifecycle.js +9 -2
  40. package/dist/plugin/lifecycle.js.map +1 -1
  41. package/dist/plugin/shell-hook-http.d.ts +39 -0
  42. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  43. package/dist/plugin/shell-hook-http.js +163 -0
  44. package/dist/plugin/shell-hook-http.js.map +1 -0
  45. package/dist/plugin/shell-hook.d.ts +93 -11
  46. package/dist/plugin/shell-hook.d.ts.map +1 -1
  47. package/dist/plugin/shell-hook.js +223 -39
  48. package/dist/plugin/shell-hook.js.map +1 -1
  49. package/dist/public-runtime.d.ts +2 -2
  50. package/dist/public-runtime.d.ts.map +1 -1
  51. package/dist/public-runtime.js +2 -2
  52. package/dist/public-runtime.js.map +1 -1
  53. package/dist/public-tools.d.ts +1 -0
  54. package/dist/public-tools.d.ts.map +1 -1
  55. package/dist/public-tools.js +2 -0
  56. package/dist/public-tools.js.map +1 -1
  57. package/dist/public-types.d.ts +1 -1
  58. package/dist/public-types.d.ts.map +1 -1
  59. package/dist/runtime/query/executor.d.ts +1 -0
  60. package/dist/runtime/query/executor.d.ts.map +1 -1
  61. package/dist/runtime/query/executor.js +34 -9
  62. package/dist/runtime/query/executor.js.map +1 -1
  63. package/dist/runtime/query/index.d.ts +2 -0
  64. package/dist/runtime/query/index.d.ts.map +1 -1
  65. package/dist/runtime/query/index.js +25 -1
  66. package/dist/runtime/query/index.js.map +1 -1
  67. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  68. package/dist/runtime/query/iteration/index.js +6 -2
  69. package/dist/runtime/query/iteration/index.js.map +1 -1
  70. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  71. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  72. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  73. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  74. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  75. package/dist/runtime/query/iteration/phases/compaction.js +6 -4
  76. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  77. package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
  78. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  79. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  80. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  81. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  82. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  83. package/dist/runtime/query/path-escalation.d.ts +2 -0
  84. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  85. package/dist/runtime/query/path-escalation.js +7 -4
  86. package/dist/runtime/query/path-escalation.js.map +1 -1
  87. package/dist/runtime/query/plugin-hooks.js +2 -2
  88. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  89. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  90. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  91. package/dist/runtime/query/prepare-turn.js +25 -0
  92. package/dist/runtime/query/prepare-turn.js.map +1 -1
  93. package/dist/runtime/query/review-policy.d.ts +3 -2
  94. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  95. package/dist/runtime/query/review-policy.js +32 -13
  96. package/dist/runtime/query/review-policy.js.map +1 -1
  97. package/dist/session/retention/archive.js +1 -1
  98. package/dist/session/retention/archive.js.map +1 -1
  99. package/dist/session/workspace/driver.d.ts +6 -0
  100. package/dist/session/workspace/driver.d.ts.map +1 -1
  101. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  102. package/dist/session/workspace/git-worktree.js +28 -0
  103. package/dist/session/workspace/git-worktree.js.map +1 -1
  104. package/dist/skills/index.d.ts +1 -1
  105. package/dist/skills/index.d.ts.map +1 -1
  106. package/dist/skills/index.js +1 -1
  107. package/dist/skills/index.js.map +1 -1
  108. package/dist/skills/loader.d.ts +7 -0
  109. package/dist/skills/loader.d.ts.map +1 -1
  110. package/dist/skills/loader.js +53 -0
  111. package/dist/skills/loader.js.map +1 -1
  112. package/dist/skills/registry.d.ts +1 -1
  113. package/dist/skills/registry.d.ts.map +1 -1
  114. package/dist/skills/registry.js +2 -1
  115. package/dist/skills/registry.js.map +1 -1
  116. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  117. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  118. package/dist/tools/builtins/bash-cwd.js +127 -0
  119. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  120. package/dist/tools/builtins/bash.d.ts.map +1 -1
  121. package/dist/tools/builtins/bash.js +41 -6
  122. package/dist/tools/builtins/bash.js.map +1 -1
  123. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  124. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  125. package/dist/tools/builtins/bounded-file-read.js +67 -0
  126. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  127. package/dist/tools/builtins/grep.d.ts +6 -1
  128. package/dist/tools/builtins/grep.d.ts.map +1 -1
  129. package/dist/tools/builtins/grep.js +187 -24
  130. package/dist/tools/builtins/grep.js.map +1 -1
  131. package/dist/tools/builtins/read-file.d.ts +1 -0
  132. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  133. package/dist/tools/builtins/read-file.js +113 -28
  134. package/dist/tools/builtins/read-file.js.map +1 -1
  135. package/dist/tools/builtins/read-media.d.ts +68 -0
  136. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  137. package/dist/tools/builtins/read-media.js +397 -0
  138. package/dist/tools/builtins/read-media.js.map +1 -0
  139. package/dist/tools/builtins/search-tools.js +1 -1
  140. package/dist/tools/builtins/search-tools.js.map +1 -1
  141. package/dist/tools/builtins/skill.d.ts +4 -2
  142. package/dist/tools/builtins/skill.d.ts.map +1 -1
  143. package/dist/tools/builtins/skill.js +61 -2
  144. package/dist/tools/builtins/skill.js.map +1 -1
  145. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  146. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  147. package/dist/tools/builtins/sleep-guard.js +33 -0
  148. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  149. package/dist/tools/builtins/view-image.d.ts +23 -1
  150. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  151. package/dist/tools/builtins/view-image.js +66 -58
  152. package/dist/tools/builtins/view-image.js.map +1 -1
  153. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  154. package/dist/tools/file-read-tracker.js +31 -0
  155. package/dist/tools/file-read-tracker.js.map +1 -1
  156. package/dist/toolsets/manager.js +3 -3
  157. package/dist/toolsets/manager.js.map +1 -1
  158. package/dist/types/agent/query.d.ts +2 -0
  159. package/dist/types/agent/query.d.ts.map +1 -1
  160. package/dist/types/agent/task.d.ts +6 -2
  161. package/dist/types/agent/task.d.ts.map +1 -1
  162. package/dist/types/agent/task.js.map +1 -1
  163. package/dist/types/connector/mcp.d.ts +46 -0
  164. package/dist/types/connector/mcp.d.ts.map +1 -1
  165. package/dist/types/hitl/index.d.ts +1 -1
  166. package/dist/types/plugin/index.d.ts +72 -5
  167. package/dist/types/plugin/index.d.ts.map +1 -1
  168. package/dist/types/plugin/index.js +12 -1
  169. package/dist/types/plugin/index.js.map +1 -1
  170. package/dist/types/session/sub-session.d.ts +6 -2
  171. package/dist/types/session/sub-session.d.ts.map +1 -1
  172. package/dist/types/skills/index.d.ts +36 -0
  173. package/dist/types/skills/index.d.ts.map +1 -1
  174. package/dist/types/skills/index.js +9 -0
  175. package/dist/types/skills/index.js.map +1 -1
  176. package/dist/types/tool/index.d.ts +22 -0
  177. package/dist/types/tool/index.d.ts.map +1 -1
  178. package/dist/types/tool/index.js.map +1 -1
  179. package/dist/types/workspace/ref.d.ts +2 -0
  180. package/dist/types/workspace/ref.d.ts.map +1 -1
  181. package/dist/utils/frontmatter.d.ts +12 -0
  182. package/dist/utils/frontmatter.d.ts.map +1 -1
  183. package/dist/utils/frontmatter.js +6 -0
  184. package/dist/utils/frontmatter.js.map +1 -1
  185. package/package.json +1 -1
  186. package/src/agents/QueryAgent.ts +2 -0
  187. package/src/agents/file-definitions.ts +126 -6
  188. package/src/authorization/protected-paths.ts +30 -2
  189. package/src/connector/mcp/client.ts +87 -1
  190. package/src/constants/mcp/index.ts +3 -0
  191. package/src/constants/plugin/index.ts +2 -0
  192. package/src/manager/agent/lifecycle.ts +24 -5
  193. package/src/manager/session/turn-recorder.ts +2 -1
  194. package/src/persona/assembler.ts +2 -2
  195. package/src/plugin/index.ts +5 -0
  196. package/src/plugin/lifecycle.ts +16 -2
  197. package/src/plugin/shell-hook-http.ts +181 -0
  198. package/src/plugin/shell-hook.ts +386 -89
  199. package/src/public-runtime.ts +2 -0
  200. package/src/public-tools.ts +2 -0
  201. package/src/public-types.ts +5 -0
  202. package/src/runtime/query/executor.ts +40 -10
  203. package/src/runtime/query/index.ts +33 -0
  204. package/src/runtime/query/iteration/index.ts +11 -3
  205. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  206. package/src/runtime/query/iteration/phases/compaction.ts +21 -4
  207. package/src/runtime/query/iteration/phases/context.ts +2 -0
  208. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  209. package/src/runtime/query/path-escalation.ts +11 -5
  210. package/src/runtime/query/plugin-hooks.ts +2 -2
  211. package/src/runtime/query/prepare-turn.ts +33 -0
  212. package/src/runtime/query/review-policy.ts +45 -16
  213. package/src/session/retention/archive.ts +1 -1
  214. package/src/session/workspace/driver.ts +6 -0
  215. package/src/session/workspace/git-worktree.ts +31 -0
  216. package/src/skills/index.ts +6 -1
  217. package/src/skills/loader.ts +54 -0
  218. package/src/skills/registry.ts +7 -6
  219. package/src/tools/builtins/bash-cwd.ts +156 -0
  220. package/src/tools/builtins/bash.ts +79 -34
  221. package/src/tools/builtins/bounded-file-read.ts +80 -0
  222. package/src/tools/builtins/grep.ts +198 -28
  223. package/src/tools/builtins/read-file.ts +151 -30
  224. package/src/tools/builtins/read-media.ts +483 -0
  225. package/src/tools/builtins/search-tools.ts +1 -1
  226. package/src/tools/builtins/skill.ts +91 -2
  227. package/src/tools/builtins/sleep-guard.ts +31 -0
  228. package/src/tools/builtins/view-image.ts +74 -62
  229. package/src/tools/file-read-tracker.ts +30 -0
  230. package/src/toolsets/manager.ts +3 -3
  231. package/src/types/agent/query.ts +2 -0
  232. package/src/types/agent/task.ts +6 -2
  233. package/src/types/connector/mcp.ts +48 -0
  234. package/src/types/hitl/index.ts +1 -1
  235. package/src/types/plugin/index.ts +102 -16
  236. package/src/types/session/sub-session.ts +6 -2
  237. package/src/types/skills/index.ts +48 -0
  238. package/src/types/tool/index.ts +22 -0
  239. package/src/types/workspace/ref.ts +2 -0
  240. package/src/utils/frontmatter.ts +21 -0
@@ -1,14 +1,23 @@
1
- import { readFile, stat } from 'node:fs/promises'
1
+ import { stat } from 'node:fs/promises'
2
2
  import { extname } from 'node:path'
3
3
  import { z } from 'zod'
4
- import type { ToolContext } from '../../types/tool/index.js'
4
+ import type { ToolContext, ToolResult } from '../../types/tool/index.js'
5
5
  import type { ToolFacts } from '../../types/tool/presentation.js'
6
6
  import { defineTool } from '../defineTool.js'
7
7
  import { resolveWithinAnyReal, toolRoots } from '../paths.js'
8
8
  import { resolveRetainedOutput } from '../retained-output.js'
9
9
  import { looksBinary } from '../text-window.js'
10
+ import { TEXT_MAX_INPUT_BYTES, readBoundedChunks, readBoundedFile } from './bounded-file-read.js'
10
11
  import { fingerprintContent } from './content-fingerprint.js'
12
+ import {
13
+ type MediaRead,
14
+ NOTEBOOK_MAX_BYTES,
15
+ PDF_MAX_BYTES,
16
+ readNotebook,
17
+ readPdfPages,
18
+ } from './read-media.js'
11
19
  import { type RenderedRead, humanBytes, renderCappedRead } from './read-render.js'
20
+ import { MAX_BYTES as IMAGE_MAX_BYTES, inspectImageBytes } from './view-image.js'
12
21
 
13
22
  const inputSchema = z.object({
14
23
  path: z.string().describe('Path to the file to read (absolute or relative)'),
@@ -35,6 +44,12 @@ const inputSchema = z.object({
35
44
  .optional()
36
45
  .describe('Starting line number (0-indexed). Defaults to 0 (beginning of file).'),
37
46
  limit: z.coerce.number().optional().describe('Maximum number of lines to read'),
47
+ pages: z
48
+ .string()
49
+ .optional()
50
+ .describe(
51
+ 'For a PDF: the pages to render as images, one page ("3") or one range ("1-5"), at most 20 pages per request. Required for a PDF of more than 10 pages.',
52
+ ),
38
53
  allow_large: z
39
54
  .boolean()
40
55
  .optional()
@@ -46,7 +61,7 @@ const inputSchema = z.object({
46
61
  export const ReadFileTool = defineTool({
47
62
  name: 'read',
48
63
  description:
49
- 'Reads a file and returns its contents with line numbers. Supports readRange ([start,end], 1-indexed inclusive) or offset/limit for large files. Without a window it returns the first 2000 lines and says so — pass offset/limit to continue. A file over 256 KiB is refused unless a window is named, and a result over about 25,000 tokens is cut to a first page that names the next call; pass allow_large only when you need all of it.',
64
+ 'Reads a file and returns its contents with line numbers. Also reads images (PNG, JPEG, WebP), which come back as pictures; PDFs, whose pages come back as images (pass pages, for example "1-5", at most 20 pages per request; a PDF of more than 10 pages needs it); and Jupyter notebooks (.ipynb), which come back as cells with their outputs. Supports readRange ([start,end], 1-indexed inclusive) or offset/limit for large files. Without a window it returns the first 2000 lines and says so — pass offset/limit to continue. A file over 256 KiB is refused unless a window is named, and a result over about 25,000 tokens is cut to a first page that names the next call; pass allow_large only when you need all of it.',
50
65
  inputSchema,
51
66
  category: 'filesystem',
52
67
  // Declared so a path outside the turn's roots can be reviewed before the
@@ -78,8 +93,17 @@ export const ReadFileTool = defineTool({
78
93
  const retained = await resolveRetainedOutput(context.retainedOutputDirectory, input.path)
79
94
  // Sandbox-aware: route through sandbox.readFile() when available
80
95
  if (context.sandbox && retained === undefined) {
81
- const buffer = await context.sandbox.readFile(input.path)
82
- const binaryGuidance = describeStructuredBinaryRead(input.path, buffer)
96
+ const sandbox = context.sandbox
97
+ const buffer = await readBoundedChunks(
98
+ (offset, length) =>
99
+ sandbox.readFile(input.path, { offset, length, signal: context.abortSignal }),
100
+ PDF_MAX_BYTES,
101
+ context.abortSignal,
102
+ (prefix) => inputLimit(input.path, prefix),
103
+ )
104
+ const media = await readMediaFile(buffer, input.path, input, context, true)
105
+ if (media) return media
106
+ const binaryGuidance = describeStructuredBinaryRead(input.path)
83
107
  if (binaryGuidance) {
84
108
  return {
85
109
  success: false,
@@ -121,15 +145,22 @@ export const ReadFileTool = defineTool({
121
145
  }
122
146
 
123
147
  const filePath = retained ?? (await resolveWithinAnyReal(toolRoots(context), input.path))
148
+ // Pictures, PDFs and notebooks are not text: they have their own size
149
+ // limits and are never numbered, so the text cap below does not apply.
150
+ const namesMediaFile = namesMedia(filePath)
124
151
  // Refused on the size alone, before the file is in memory, when no window
125
152
  // was named: the cap exists so a huge file is never loaded to be refused.
126
- if (input.readRange === undefined && input.limit === undefined) {
153
+ if (!namesMediaFile && input.readRange === undefined && input.limit === undefined) {
127
154
  const { size } = await stat(filePath)
128
155
  const early = renderCappedRead('', size, input, capOptions(input, context))
129
156
  if (!early.ok) return refusedRead(filePath, size, early.message, false)
130
157
  }
131
- const buffer = await readFile(filePath)
132
- const binaryGuidance = describeStructuredBinaryRead(filePath, buffer)
158
+ const buffer = await readBoundedFile(filePath, PDF_MAX_BYTES, context.abortSignal, (prefix) =>
159
+ inputLimit(filePath, prefix),
160
+ )
161
+ const media = await readMediaFile(buffer, filePath, input, context, false)
162
+ if (media) return media
163
+ const binaryGuidance = describeStructuredBinaryRead(filePath)
133
164
  if (binaryGuidance) {
134
165
  return {
135
166
  success: false,
@@ -172,6 +203,117 @@ export const ReadFileTool = defineTool({
172
203
  },
173
204
  })
174
205
 
206
+ type MediaKind = 'image' | 'pdf' | 'notebook'
207
+
208
+ /**
209
+ * What a file is by its bytes, never by its name: a text file called `.png` is
210
+ * still text. A notebook is JSON, so only its extension can name it.
211
+ */
212
+ function mediaKindOf(path: string, buffer: Buffer): MediaKind | undefined {
213
+ if (buffer.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'))) return 'image'
214
+ if (buffer[0] === 0xff && buffer[1] === 0xd8) return 'image'
215
+ if (buffer.toString('ascii', 0, 4) === 'RIFF' && buffer.toString('ascii', 8, 12) === 'WEBP')
216
+ return 'image'
217
+ if (buffer.subarray(0, 4).toString('utf8') === '%PDF') return 'pdf'
218
+ if (extname(path).toLowerCase() === '.ipynb') return 'notebook'
219
+ return undefined
220
+ }
221
+
222
+ /** A name that suggests a file the text cap should not judge. */
223
+ function namesMedia(path: string): boolean {
224
+ return ['.png', '.jpg', '.jpeg', '.webp', '.pdf', '.ipynb'].includes(extname(path).toLowerCase())
225
+ }
226
+
227
+ function inputLimit(path: string, prefix: Buffer): number {
228
+ const kind = mediaKindOf(path, prefix)
229
+ return kind ? mediaLimit(kind) : TEXT_MAX_INPUT_BYTES
230
+ }
231
+
232
+ function mediaLimit(kind: MediaKind): number {
233
+ return kind === 'image' ? IMAGE_MAX_BYTES : kind === 'pdf' ? PDF_MAX_BYTES : NOTEBOOK_MAX_BYTES
234
+ }
235
+
236
+ function mediaFailure(path: string, bytes: number, message: string, sandboxed: boolean) {
237
+ return {
238
+ success: false as const,
239
+ output: message,
240
+ error: message,
241
+ data: { path, ...(sandboxed ? { sandboxed: true } : {}) },
242
+ facts: { kind: 'read' as const, path, bytes },
243
+ }
244
+ }
245
+
246
+ function tooLargeMedia(path: string, bytes: number, kind: MediaKind) {
247
+ const what = kind === 'image' ? 'Image' : kind === 'pdf' ? 'PDF' : 'Notebook'
248
+ return mediaFailure(
249
+ path,
250
+ bytes,
251
+ `${what} file (${humanBytes(bytes)}) exceeds the maximum size this tool can read (${humanBytes(mediaLimit(kind))}).`,
252
+ false,
253
+ )
254
+ }
255
+
256
+ /**
257
+ * An image, a PDF or a notebook, as what the model can use: the picture itself,
258
+ * the pages of the PDF as pictures, or the notebook's cells with their output.
259
+ * Undefined for any other file, which is read as text.
260
+ */
261
+ async function readMediaFile(
262
+ buffer: Buffer,
263
+ path: string,
264
+ input: { readonly pages?: string | undefined },
265
+ context: ToolContext,
266
+ sandboxed: boolean,
267
+ ): Promise<ToolResult | undefined> {
268
+ const kind = mediaKindOf(path, buffer)
269
+ if (!kind) return undefined
270
+ if (buffer.length > mediaLimit(kind)) return tooLargeMedia(path, buffer.length, kind)
271
+ if (kind === 'image') {
272
+ try {
273
+ const result = await inspectImageBytes(
274
+ { path, bytes: buffer, sandboxed },
275
+ context.abortSignal,
276
+ )
277
+ return {
278
+ ...result,
279
+ facts: { kind: 'read' as const, path, bytes: buffer.length },
280
+ }
281
+ } catch (error) {
282
+ return mediaFailure(
283
+ path,
284
+ buffer.length,
285
+ error instanceof Error ? error.message : String(error),
286
+ sandboxed,
287
+ )
288
+ }
289
+ }
290
+ const media: MediaRead =
291
+ kind === 'pdf'
292
+ ? await readPdfPages(buffer, input.pages, path, context.abortSignal)
293
+ : readNotebook(buffer, path)
294
+ if (!media.ok) return mediaFailure(path, buffer.length, media.message, sandboxed)
295
+ const text = media.parts
296
+ .filter((part) => part.type === 'text')
297
+ .map((part) => part.text)
298
+ .join('\n')
299
+ const hasImage = media.parts.some((part) => part.type === 'image')
300
+ return {
301
+ success: true as const,
302
+ output: text,
303
+ ...(hasImage
304
+ ? {
305
+ content: media.parts.map((part) =>
306
+ part.type === 'text'
307
+ ? { type: 'text' as const, text: part.text }
308
+ : { type: 'image' as const, mediaType: part.mediaType, data: part.data },
309
+ ),
310
+ }
311
+ : {}),
312
+ data: { path, ...(sandboxed ? { sandboxed: true } : {}), ...media.facts },
313
+ facts: { kind: 'read' as const, path, bytes: buffer.length },
314
+ }
315
+ }
316
+
175
317
  function capOptions(
176
318
  input: { path: string; allow_large?: boolean | undefined },
177
319
  context: ToolContext,
@@ -315,35 +457,14 @@ function unchangedRead(
315
457
  }
316
458
  }
317
459
 
318
- function describeStructuredBinaryRead(path: string, buffer: Buffer): string | null {
460
+ function describeStructuredBinaryRead(path: string): string | null {
319
461
  const ext = extname(path).toLowerCase()
320
- const image = buffer.subarray(0, 8).equals(Buffer.from('89504e470d0a1a0a', 'hex'))
321
- ? 'PNG'
322
- : buffer[0] === 0xff && buffer[1] === 0xd8
323
- ? 'JPEG'
324
- : buffer.toString('ascii', 0, 4) === 'RIFF' && buffer.toString('ascii', 8, 12) === 'WEBP'
325
- ? 'WebP'
326
- : null
327
- if (image) {
328
- return [
329
- `The file "${path}" is a ${image} raster image, not UTF-8 text.`,
330
- 'Use an available view_image tool with this path to inspect the actual pixels. Reading binary bytes as text does not provide visual evidence.',
331
- 'If image inspection is unavailable, open the file in an application and use an available screenshot tool; report the limitation if neither can show the image.',
332
- ].join('\n')
333
- }
334
462
  if (ext === '.docx') return buildStructuredBinaryGuidance(path, 'DOCX', 'python-docx')
335
463
  if (ext === '.pptx') return buildStructuredBinaryGuidance(path, 'PPTX', 'python-pptx')
336
464
  if (ext === '.xlsx') return buildStructuredBinaryGuidance(path, 'XLSX', 'openpyxl')
337
- if (ext === '.pdf' || startsWithPdfHeader(buffer)) {
338
- return buildStructuredBinaryGuidance(path, 'PDF', 'pdftotext or PyMuPDF')
339
- }
340
465
  return null
341
466
  }
342
467
 
343
- function startsWithPdfHeader(buffer: Buffer): boolean {
344
- return buffer.length >= 4 && buffer.subarray(0, 4).toString('utf8') === '%PDF'
345
- }
346
-
347
468
  function buildStructuredBinaryGuidance(path: string, format: string, extractor: string): string {
348
469
  return [
349
470
  `The file "${path}" is a ${format} document package, not UTF-8 text.`,