@namzu/sdk 57.0.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 (278) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/agents/QueryAgent.d.ts.map +1 -1
  3. package/dist/agents/QueryAgent.js +3 -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/compaction/index.d.ts +1 -0
  14. package/dist/compaction/index.d.ts.map +1 -1
  15. package/dist/compaction/index.js.map +1 -1
  16. package/dist/compaction/manual.d.ts +7 -0
  17. package/dist/compaction/manual.d.ts.map +1 -1
  18. package/dist/compaction/manual.js +31 -12
  19. package/dist/compaction/manual.js.map +1 -1
  20. package/dist/compaction/model-summary.d.ts +101 -0
  21. package/dist/compaction/model-summary.d.ts.map +1 -0
  22. package/dist/compaction/model-summary.js +333 -0
  23. package/dist/compaction/model-summary.js.map +1 -0
  24. package/dist/compaction/verifier.d.ts +6 -1
  25. package/dist/compaction/verifier.d.ts.map +1 -1
  26. package/dist/compaction/verifier.js +3 -3
  27. package/dist/compaction/verifier.js.map +1 -1
  28. package/dist/config/runtime.d.ts +62 -0
  29. package/dist/config/runtime.d.ts.map +1 -1
  30. package/dist/config/runtime.js +16 -0
  31. package/dist/config/runtime.js.map +1 -1
  32. package/dist/connector/mcp/client.d.ts +8 -0
  33. package/dist/connector/mcp/client.d.ts.map +1 -1
  34. package/dist/connector/mcp/client.js +75 -2
  35. package/dist/connector/mcp/client.js.map +1 -1
  36. package/dist/constants/mcp/index.d.ts +2 -0
  37. package/dist/constants/mcp/index.d.ts.map +1 -1
  38. package/dist/constants/mcp/index.js +2 -0
  39. package/dist/constants/mcp/index.js.map +1 -1
  40. package/dist/constants/plugin/index.d.ts +2 -0
  41. package/dist/constants/plugin/index.d.ts.map +1 -1
  42. package/dist/constants/plugin/index.js +2 -0
  43. package/dist/constants/plugin/index.js.map +1 -1
  44. package/dist/manager/agent/lifecycle.d.ts +1 -1
  45. package/dist/manager/agent/lifecycle.d.ts.map +1 -1
  46. package/dist/manager/agent/lifecycle.js +25 -4
  47. package/dist/manager/agent/lifecycle.js.map +1 -1
  48. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  49. package/dist/manager/session/turn-recorder.js +2 -1
  50. package/dist/manager/session/turn-recorder.js.map +1 -1
  51. package/dist/persona/assembler.d.ts.map +1 -1
  52. package/dist/persona/assembler.js +2 -2
  53. package/dist/persona/assembler.js.map +1 -1
  54. package/dist/plugin/index.d.ts +1 -1
  55. package/dist/plugin/index.d.ts.map +1 -1
  56. package/dist/plugin/lifecycle.d.ts +8 -1
  57. package/dist/plugin/lifecycle.d.ts.map +1 -1
  58. package/dist/plugin/lifecycle.js +9 -2
  59. package/dist/plugin/lifecycle.js.map +1 -1
  60. package/dist/plugin/shell-hook-http.d.ts +39 -0
  61. package/dist/plugin/shell-hook-http.d.ts.map +1 -0
  62. package/dist/plugin/shell-hook-http.js +163 -0
  63. package/dist/plugin/shell-hook-http.js.map +1 -0
  64. package/dist/plugin/shell-hook.d.ts +93 -11
  65. package/dist/plugin/shell-hook.d.ts.map +1 -1
  66. package/dist/plugin/shell-hook.js +223 -39
  67. package/dist/plugin/shell-hook.js.map +1 -1
  68. package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
  69. package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
  70. package/dist/prompt/coding-agent-doctrine.js +2 -1
  71. package/dist/prompt/coding-agent-doctrine.js.map +1 -1
  72. package/dist/public-runtime.d.ts +3 -2
  73. package/dist/public-runtime.d.ts.map +1 -1
  74. package/dist/public-runtime.js +2 -2
  75. package/dist/public-runtime.js.map +1 -1
  76. package/dist/public-tools.d.ts +1 -0
  77. package/dist/public-tools.d.ts.map +1 -1
  78. package/dist/public-tools.js +2 -0
  79. package/dist/public-tools.js.map +1 -1
  80. package/dist/public-types.d.ts +1 -1
  81. package/dist/public-types.d.ts.map +1 -1
  82. package/dist/runtime/query/executor.d.ts +4 -0
  83. package/dist/runtime/query/executor.d.ts.map +1 -1
  84. package/dist/runtime/query/executor.js +47 -9
  85. package/dist/runtime/query/executor.js.map +1 -1
  86. package/dist/runtime/query/index.d.ts +8 -0
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +29 -1
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  91. package/dist/runtime/query/iteration/index.js +6 -2
  92. package/dist/runtime/query/iteration/index.js.map +1 -1
  93. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
  94. package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
  95. package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
  96. package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
  97. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  98. package/dist/runtime/query/iteration/phases/compaction.js +52 -5
  99. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  100. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  101. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  102. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  103. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  104. package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
  105. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  106. package/dist/runtime/query/path-escalation.d.ts +2 -0
  107. package/dist/runtime/query/path-escalation.d.ts.map +1 -1
  108. package/dist/runtime/query/path-escalation.js +7 -4
  109. package/dist/runtime/query/path-escalation.js.map +1 -1
  110. package/dist/runtime/query/plugin-hooks.js +2 -2
  111. package/dist/runtime/query/plugin-hooks.js.map +1 -1
  112. package/dist/runtime/query/prepare-turn.d.ts +2 -0
  113. package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
  114. package/dist/runtime/query/prepare-turn.js +25 -0
  115. package/dist/runtime/query/prepare-turn.js.map +1 -1
  116. package/dist/runtime/query/review-policy.d.ts +3 -2
  117. package/dist/runtime/query/review-policy.d.ts.map +1 -1
  118. package/dist/runtime/query/review-policy.js +32 -13
  119. package/dist/runtime/query/review-policy.js.map +1 -1
  120. package/dist/scheduler/local.js +1 -1
  121. package/dist/scheduler/local.js.map +1 -1
  122. package/dist/session/retention/archive.js +1 -1
  123. package/dist/session/retention/archive.js.map +1 -1
  124. package/dist/session/workspace/driver.d.ts +6 -0
  125. package/dist/session/workspace/driver.d.ts.map +1 -1
  126. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  127. package/dist/session/workspace/git-worktree.js +28 -0
  128. package/dist/session/workspace/git-worktree.js.map +1 -1
  129. package/dist/skills/index.d.ts +1 -1
  130. package/dist/skills/index.d.ts.map +1 -1
  131. package/dist/skills/index.js +1 -1
  132. package/dist/skills/index.js.map +1 -1
  133. package/dist/skills/loader.d.ts +7 -0
  134. package/dist/skills/loader.d.ts.map +1 -1
  135. package/dist/skills/loader.js +53 -0
  136. package/dist/skills/loader.js.map +1 -1
  137. package/dist/skills/registry.d.ts +1 -1
  138. package/dist/skills/registry.d.ts.map +1 -1
  139. package/dist/skills/registry.js +2 -1
  140. package/dist/skills/registry.js.map +1 -1
  141. package/dist/tools/builtins/bash-cwd.d.ts +52 -0
  142. package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
  143. package/dist/tools/builtins/bash-cwd.js +127 -0
  144. package/dist/tools/builtins/bash-cwd.js.map +1 -0
  145. package/dist/tools/builtins/bash.d.ts.map +1 -1
  146. package/dist/tools/builtins/bash.js +41 -6
  147. package/dist/tools/builtins/bash.js.map +1 -1
  148. package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
  149. package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
  150. package/dist/tools/builtins/bounded-file-read.js +67 -0
  151. package/dist/tools/builtins/bounded-file-read.js.map +1 -0
  152. package/dist/tools/builtins/grep.d.ts +6 -1
  153. package/dist/tools/builtins/grep.d.ts.map +1 -1
  154. package/dist/tools/builtins/grep.js +187 -24
  155. package/dist/tools/builtins/grep.js.map +1 -1
  156. package/dist/tools/builtins/read-file.d.ts +1 -0
  157. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  158. package/dist/tools/builtins/read-file.js +113 -28
  159. package/dist/tools/builtins/read-file.js.map +1 -1
  160. package/dist/tools/builtins/read-media.d.ts +68 -0
  161. package/dist/tools/builtins/read-media.d.ts.map +1 -0
  162. package/dist/tools/builtins/read-media.js +397 -0
  163. package/dist/tools/builtins/read-media.js.map +1 -0
  164. package/dist/tools/builtins/search-tools.js +1 -1
  165. package/dist/tools/builtins/search-tools.js.map +1 -1
  166. package/dist/tools/builtins/skill.d.ts +4 -2
  167. package/dist/tools/builtins/skill.d.ts.map +1 -1
  168. package/dist/tools/builtins/skill.js +61 -2
  169. package/dist/tools/builtins/skill.js.map +1 -1
  170. package/dist/tools/builtins/sleep-guard.d.ts +15 -0
  171. package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
  172. package/dist/tools/builtins/sleep-guard.js +33 -0
  173. package/dist/tools/builtins/sleep-guard.js.map +1 -0
  174. package/dist/tools/builtins/view-image.d.ts +23 -1
  175. package/dist/tools/builtins/view-image.d.ts.map +1 -1
  176. package/dist/tools/builtins/view-image.js +66 -58
  177. package/dist/tools/builtins/view-image.js.map +1 -1
  178. package/dist/tools/file-read-tracker.d.ts.map +1 -1
  179. package/dist/tools/file-read-tracker.js +31 -0
  180. package/dist/tools/file-read-tracker.js.map +1 -1
  181. package/dist/toolsets/manager.js +3 -3
  182. package/dist/toolsets/manager.js.map +1 -1
  183. package/dist/types/agent/base.d.ts +8 -0
  184. package/dist/types/agent/base.d.ts.map +1 -1
  185. package/dist/types/agent/query.d.ts +2 -0
  186. package/dist/types/agent/query.d.ts.map +1 -1
  187. package/dist/types/agent/scheduler.d.ts +8 -0
  188. package/dist/types/agent/scheduler.d.ts.map +1 -1
  189. package/dist/types/agent/task.d.ts +6 -2
  190. package/dist/types/agent/task.d.ts.map +1 -1
  191. package/dist/types/agent/task.js.map +1 -1
  192. package/dist/types/connector/mcp.d.ts +46 -0
  193. package/dist/types/connector/mcp.d.ts.map +1 -1
  194. package/dist/types/hitl/index.d.ts +1 -1
  195. package/dist/types/plugin/index.d.ts +74 -7
  196. package/dist/types/plugin/index.d.ts.map +1 -1
  197. package/dist/types/plugin/index.js +12 -1
  198. package/dist/types/plugin/index.js.map +1 -1
  199. package/dist/types/session/sub-session.d.ts +6 -2
  200. package/dist/types/session/sub-session.d.ts.map +1 -1
  201. package/dist/types/skills/index.d.ts +36 -0
  202. package/dist/types/skills/index.d.ts.map +1 -1
  203. package/dist/types/skills/index.js +9 -0
  204. package/dist/types/skills/index.js.map +1 -1
  205. package/dist/types/tool/index.d.ts +37 -0
  206. package/dist/types/tool/index.d.ts.map +1 -1
  207. package/dist/types/tool/index.js.map +1 -1
  208. package/dist/types/workspace/ref.d.ts +2 -0
  209. package/dist/types/workspace/ref.d.ts.map +1 -1
  210. package/dist/utils/frontmatter.d.ts +12 -0
  211. package/dist/utils/frontmatter.d.ts.map +1 -1
  212. package/dist/utils/frontmatter.js +6 -0
  213. package/dist/utils/frontmatter.js.map +1 -1
  214. package/package.json +1 -1
  215. package/src/agents/QueryAgent.ts +3 -0
  216. package/src/agents/file-definitions.ts +126 -6
  217. package/src/authorization/protected-paths.ts +30 -2
  218. package/src/compaction/index.ts +1 -0
  219. package/src/compaction/manual.ts +68 -30
  220. package/src/compaction/model-summary.ts +412 -0
  221. package/src/compaction/verifier.ts +3 -3
  222. package/src/config/runtime.ts +16 -0
  223. package/src/connector/mcp/client.ts +87 -1
  224. package/src/constants/mcp/index.ts +3 -0
  225. package/src/constants/plugin/index.ts +2 -0
  226. package/src/manager/agent/lifecycle.ts +24 -5
  227. package/src/manager/session/turn-recorder.ts +2 -1
  228. package/src/persona/assembler.ts +2 -2
  229. package/src/plugin/index.ts +5 -0
  230. package/src/plugin/lifecycle.ts +16 -2
  231. package/src/plugin/shell-hook-http.ts +181 -0
  232. package/src/plugin/shell-hook.ts +386 -89
  233. package/src/prompt/coding-agent-doctrine.ts +2 -1
  234. package/src/public-runtime.ts +6 -0
  235. package/src/public-tools.ts +2 -0
  236. package/src/public-types.ts +5 -0
  237. package/src/runtime/query/executor.ts +54 -10
  238. package/src/runtime/query/index.ts +44 -0
  239. package/src/runtime/query/iteration/index.ts +11 -3
  240. package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
  241. package/src/runtime/query/iteration/phases/compaction.ts +67 -5
  242. package/src/runtime/query/iteration/phases/context.ts +6 -0
  243. package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
  244. package/src/runtime/query/path-escalation.ts +11 -5
  245. package/src/runtime/query/plugin-hooks.ts +2 -2
  246. package/src/runtime/query/prepare-turn.ts +33 -0
  247. package/src/runtime/query/review-policy.ts +45 -16
  248. package/src/scheduler/local.ts +1 -1
  249. package/src/session/retention/archive.ts +1 -1
  250. package/src/session/workspace/driver.ts +6 -0
  251. package/src/session/workspace/git-worktree.ts +31 -0
  252. package/src/skills/index.ts +6 -1
  253. package/src/skills/loader.ts +54 -0
  254. package/src/skills/registry.ts +7 -6
  255. package/src/tools/builtins/bash-cwd.ts +156 -0
  256. package/src/tools/builtins/bash.ts +79 -34
  257. package/src/tools/builtins/bounded-file-read.ts +80 -0
  258. package/src/tools/builtins/grep.ts +198 -28
  259. package/src/tools/builtins/read-file.ts +151 -30
  260. package/src/tools/builtins/read-media.ts +483 -0
  261. package/src/tools/builtins/search-tools.ts +1 -1
  262. package/src/tools/builtins/skill.ts +91 -2
  263. package/src/tools/builtins/sleep-guard.ts +31 -0
  264. package/src/tools/builtins/view-image.ts +74 -62
  265. package/src/tools/file-read-tracker.ts +30 -0
  266. package/src/toolsets/manager.ts +3 -3
  267. package/src/types/agent/base.ts +9 -0
  268. package/src/types/agent/query.ts +2 -0
  269. package/src/types/agent/scheduler.ts +9 -0
  270. package/src/types/agent/task.ts +6 -2
  271. package/src/types/connector/mcp.ts +48 -0
  272. package/src/types/hitl/index.ts +1 -1
  273. package/src/types/plugin/index.ts +102 -16
  274. package/src/types/session/sub-session.ts +6 -2
  275. package/src/types/skills/index.ts +48 -0
  276. package/src/types/tool/index.ts +38 -0
  277. package/src/types/workspace/ref.ts +2 -0
  278. package/src/utils/frontmatter.ts +21 -0
@@ -1,8 +1,9 @@
1
- import { readFile, stat } from 'node:fs/promises'
1
+ import { stat } from 'node:fs/promises'
2
2
  import { isAbsolute, join, relative, sep } from 'node:path'
3
3
  import { z } from 'zod'
4
4
  import { walkFilesLocally } from '../../sandbox/file-walk.js'
5
5
  import type { SandboxWalkFilesOptions } from '../../types/sandbox/index.js'
6
+ import type { ToolContext } from '../../types/tool/index.js'
6
7
  import { subscribeToAbort } from '../../utils/abort.js'
7
8
  import { defineTool } from '../defineTool.js'
8
9
  import {
@@ -13,6 +14,7 @@ import {
13
14
  toolRoots,
14
15
  } from '../paths.js'
15
16
  import { relativePosix, resolveWithinPosix } from '../posix-path.js'
17
+ import { readBoundedChunks, readBoundedFile } from './bounded-file-read.js'
16
18
 
17
19
  /** A remote read has no signal parameter; stop consuming it when the turn ends. */
18
20
  async function readWithSignal(read: () => Promise<Buffer>, signal?: AbortSignal): Promise<Buffer> {
@@ -57,6 +59,46 @@ export const DEFAULT_HEAD_LIMIT = 250
57
59
  /** A matching or context line longer than this is replaced by a notice, not printed. */
58
60
  export const MAX_LINE_COLUMNS = 500
59
61
 
62
+ /** File kinds for the `type` argument: extensions (without the dot) and exact file names. */
63
+ const FILE_TYPES: Record<string, { ext: string[]; names?: string[] }> = {
64
+ js: { ext: ['js', 'jsx', 'mjs', 'cjs'] },
65
+ ts: { ext: ['ts', 'tsx', 'mts', 'cts'] },
66
+ py: { ext: ['py', 'pyi'] },
67
+ rust: { ext: ['rs'] },
68
+ go: { ext: ['go'] },
69
+ java: { ext: ['java'] },
70
+ kotlin: { ext: ['kt', 'kts'] },
71
+ swift: { ext: ['swift'] },
72
+ c: { ext: ['c', 'h'] },
73
+ cpp: { ext: ['cc', 'cpp', 'cxx', 'hpp', 'hh', 'hxx', 'h'] },
74
+ cs: { ext: ['cs'] },
75
+ rb: { ext: ['rb', 'rake', 'gemspec'] },
76
+ php: { ext: ['php'] },
77
+ sh: { ext: ['sh', 'bash', 'zsh'] },
78
+ md: { ext: ['md', 'markdown', 'mdx'] },
79
+ json: { ext: ['json', 'jsonc'] },
80
+ yaml: { ext: ['yml', 'yaml'] },
81
+ toml: { ext: ['toml'] },
82
+ html: { ext: ['html', 'htm'] },
83
+ css: { ext: ['css', 'scss', 'sass', 'less'] },
84
+ sql: { ext: ['sql'] },
85
+ xml: { ext: ['xml'] },
86
+ make: { ext: ['mk', 'mak'], names: ['Makefile', 'makefile', 'GNUmakefile'] },
87
+ docker: { ext: [], names: ['Dockerfile'] },
88
+ }
89
+ FILE_TYPES.javascript = FILE_TYPES.js as { ext: string[] }
90
+ FILE_TYPES.typescript = FILE_TYPES.ts as { ext: string[] }
91
+ FILE_TYPES.python = FILE_TYPES.py as { ext: string[] }
92
+ FILE_TYPES.rs = FILE_TYPES.rust as { ext: string[] }
93
+ FILE_TYPES.markdown = FILE_TYPES.md as { ext: string[] }
94
+
95
+ function matchesType(path: string, kind: { ext: string[]; names?: string[] }): boolean {
96
+ const name = path.slice(path.lastIndexOf('/') + 1)
97
+ if (kind.names?.includes(name)) return true
98
+ const dot = name.lastIndexOf('.')
99
+ return dot > 0 && kind.ext.includes(name.slice(dot + 1))
100
+ }
101
+
60
102
  const inputSchema = z.object({
61
103
  pattern: z.string().describe('Regular expression pattern to search for in file contents'),
62
104
  path: z
@@ -74,6 +116,24 @@ const inputSchema = z.object({
74
116
  .min(0)
75
117
  .default(0)
76
118
  .describe('Number of lines to show before and after each match'),
119
+ before_context: z.coerce
120
+ .number()
121
+ .int()
122
+ .min(0)
123
+ .optional()
124
+ .describe('Lines before each content match; overrides context_lines for this side.'),
125
+ after_context: z.coerce
126
+ .number()
127
+ .int()
128
+ .min(0)
129
+ .optional()
130
+ .describe('Lines after each content match; overrides context_lines for this side.'),
131
+ only_matching: z
132
+ .boolean()
133
+ .optional()
134
+ .describe(
135
+ 'In content mode, print each matched substring instead of the whole line. Ignores context.',
136
+ ),
77
137
  output_mode: z
78
138
  .enum(['files_with_matches', 'content', 'count'])
79
139
  .default('files_with_matches')
@@ -94,6 +154,18 @@ const inputSchema = z.object({
94
154
  .min(0)
95
155
  .default(0)
96
156
  .describe('Skip this many entries before applying head_limit, to page through results.'),
157
+ multiline: z
158
+ .boolean()
159
+ .optional()
160
+ .describe(
161
+ 'Let a pattern span lines: "." also matches a newline, and "^" and "$" match at line starts and ends. Content mode prints every line a match covers.',
162
+ ),
163
+ type: z
164
+ .string()
165
+ .optional()
166
+ .describe(
167
+ 'Only search files of this kind, e.g. "js", "ts", "py", "rust", "go", "java", "md". Narrows what include selects.',
168
+ ),
97
169
  max_results: z.coerce
98
170
  .number()
99
171
  .int()
@@ -117,7 +189,7 @@ function isBinaryContent(buffer: Buffer): boolean {
117
189
  export const GrepTool = defineTool({
118
190
  name: 'grep',
119
191
  description:
120
- 'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches with a bounded traversal and identifies incomplete searches. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
192
+ 'Searches file contents using a regular expression. By default returns the paths of matching files, newest first; output_mode "content" returns matching lines with file paths, line numbers and optional context lines, and "count" returns the number of matching lines per file. Set multiline for a pattern that spans lines and type (js, py, rust, ...) to search one kind of file. In content mode, before_context and after_context override symmetric context_lines; only_matching returns matched substrings without context. Results are paged: head_limit (default 250, 0 for none) and offset. A line over 500 characters is replaced by a notice. Searches with a bounded traversal and identifies incomplete searches. Skips binary files, files over 5 MB, and symlinks. Choose the narrowest relevant directory.',
121
193
  inputSchema,
122
194
  category: 'analysis',
123
195
  // Declared so a path outside the turn's roots can be reviewed before the
@@ -146,7 +218,18 @@ export const GrepTool = defineTool({
146
218
 
147
219
  async execute(input, context) {
148
220
  context.abortSignal?.throwIfAborted()
149
- const flags = input.case_sensitive ? 'g' : 'gi'
221
+ const multiline = input.multiline === true
222
+ const flags = `${(input.case_sensitive ?? true) ? 'g' : 'gi'}${multiline ? 'ms' : ''}`
223
+ const fileType = input.type
224
+ ? FILE_TYPES[input.type.toLowerCase().replace(/^\./, '')]
225
+ : undefined
226
+ if (input.type && !fileType) {
227
+ return {
228
+ success: false,
229
+ output: '',
230
+ error: `Unknown file type "${input.type}". Known types: ${Object.keys(FILE_TYPES).sort().join(', ')}. Use include for another pattern.`,
231
+ }
232
+ }
150
233
  let regex: RegExp
151
234
  try {
152
235
  regex = new RegExp(input.pattern, flags)
@@ -189,6 +272,8 @@ export const GrepTool = defineTool({
189
272
  // `max_results` predates `head_limit`; it keeps meaning "at most this many".
190
273
  const headLimit = input.head_limit ?? input.max_results ?? DEFAULT_HEAD_LIMIT
191
274
  const offset = input.offset ?? 0
275
+ const beforeContext = input.before_context ?? input.context_lines ?? 0
276
+ const afterContext = input.after_context ?? input.context_lines ?? 0
192
277
  // Content lines in output order. Only content mode uses them as the
193
278
  // result; the other modes build from `matchedFiles`.
194
279
  const results: string[] = []
@@ -213,6 +298,7 @@ export const GrepTool = defineTool({
213
298
  )
214
299
  }
215
300
  filesSearched++
301
+ if (fileType && !matchesType(entry.path, fileType)) continue
216
302
  if (entry.size > MAX_FILE_SIZE) continue
217
303
 
218
304
  let content: string
@@ -221,10 +307,15 @@ export const GrepTool = defineTool({
221
307
  const buffer = await readWithSignal(
222
308
  () =>
223
309
  sandbox
224
- ? sandbox.readFile(filePath)
310
+ ? readBoundedChunks(
311
+ (offset, length) =>
312
+ sandbox.readFile(filePath, { offset, length, signal: context.abortSignal }),
313
+ MAX_FILE_SIZE,
314
+ context.abortSignal,
315
+ )
225
316
  : resolveWithinReal(root, filePath).then((path) => {
226
317
  contained = path
227
- return readFile(path, { signal: context.abortSignal })
318
+ return readBoundedFile(path, MAX_FILE_SIZE, context.abortSignal)
228
319
  }),
229
320
  context.abortSignal,
230
321
  )
@@ -245,36 +336,47 @@ export const GrepTool = defineTool({
245
336
  .split(sep)
246
337
  .join('/')}`
247
338
 
248
- for (let i = 0; i < lines.length; i++) {
249
- context.abortSignal?.throwIfAborted()
250
- const line = lines[i] ?? ''
251
- regex.lastIndex = 0
252
- if (!regex.test(line)) continue
339
+ if (mode === 'content' && input.only_matching) {
340
+ for (const fragment of matchingFragments(content, regex, multiline)) {
341
+ context.abortSignal?.throwIfAborted()
342
+ if (fileMatches++ === 0) filesMatched++
343
+ totalMatches++
344
+ results.push(`${relPath}:${fragment.line}:${clipLine(fragment.text, 'matching')}`)
345
+ if (headLimit > 0 && results.length >= offset + headLimit) {
346
+ pageHasMore = true
347
+ break
348
+ }
349
+ }
350
+ if (pageHasMore) break
351
+ continue
352
+ }
253
353
 
354
+ // Each hit is the first and last line (0-based) a match covers.
355
+ const hits = multiline ? multilineHits(content, regex) : lineHits(lines, regex, context)
356
+ // Count mode reports matching lines, so a line covered twice counts once.
357
+ const countedLines = new Set<number>()
358
+ for (const [first, last] of hits) {
359
+ context.abortSignal?.throwIfAborted()
254
360
  if (fileMatches === 0) filesMatched++
255
361
  fileMatches++
256
362
  totalMatches++
257
363
  // A file list only needs to know that the file matches.
258
364
  if (mode === 'files_with_matches') break
259
- if (mode === 'count') continue
260
-
261
- if (input.context_lines > 0) {
262
- const start = Math.max(0, i - input.context_lines)
263
- const end = Math.min(lines.length - 1, i + input.context_lines)
264
-
265
- if (results.length > 0) {
266
- results.push('--')
267
- }
365
+ if (mode === 'count') {
366
+ for (let j = first; j <= last; j++) countedLines.add(j)
367
+ continue
368
+ }
268
369
 
269
- for (let j = start; j <= end; j++) {
270
- const prefix = j === i ? ':' : '-'
271
- const text = lines[j] ?? ''
272
- results.push(
273
- `${relPath}${prefix}${j + 1}${prefix}${clipLine(text, j === i ? 'matching' : 'context')}`,
274
- )
275
- }
276
- } else {
277
- results.push(`${relPath}:${i + 1}:${clipLine(line, 'matching')}`)
370
+ const before = Math.max(0, first - beforeContext)
371
+ const after = Math.min(lines.length - 1, last + afterContext)
372
+ if ((beforeContext > 0 || afterContext > 0) && results.length > 0) results.push('--')
373
+ for (let j = before; j <= after; j++) {
374
+ if (headLimit > 0 && results.length >= offset + headLimit) break
375
+ const hit = j >= first && j <= last
376
+ const prefix = hit ? ':' : '-'
377
+ results.push(
378
+ `${relPath}${prefix}${j + 1}${prefix}${clipLine(lines[j] ?? '', hit ? 'matching' : 'context')}`,
379
+ )
278
380
  }
279
381
 
280
382
  // A full page ends the search: another file is not read just to
@@ -285,6 +387,7 @@ export const GrepTool = defineTool({
285
387
  break
286
388
  }
287
389
  }
390
+ if (mode === 'count' && fileMatches > 0) fileMatches = countedLines.size
288
391
 
289
392
  if (fileMatches > 0 && mode !== 'content') {
290
393
  matchedFiles.push({
@@ -404,3 +507,70 @@ async function sortByNewest<T extends { relPath: string; host?: string }>(
404
507
  .sort((a, b) => b.mtime - a.mtime || a.file.relPath.localeCompare(b.file.relPath))
405
508
  .map((entry) => entry.file)
406
509
  }
510
+
511
+ /** One hit per matching line. */
512
+ function lineHits(
513
+ lines: readonly string[],
514
+ regex: RegExp,
515
+ context: ToolContext,
516
+ ): [number, number][] {
517
+ const hits: [number, number][] = []
518
+ for (let i = 0; i < lines.length; i++) {
519
+ context.abortSignal?.throwIfAborted()
520
+ regex.lastIndex = 0
521
+ if (regex.test(lines[i] ?? '')) hits.push([i, i])
522
+ }
523
+ return hits
524
+ }
525
+
526
+ /** One hit per match over the whole text, from the line it starts on to the line it ends on. */
527
+ function multilineHits(content: string, regex: RegExp): [number, number][] {
528
+ const hits: [number, number][] = []
529
+ const starts = [0]
530
+ for (let i = content.indexOf('\n'); i !== -1; i = content.indexOf('\n', i + 1)) starts.push(i + 1)
531
+ const lineOf = (offset: number): number => {
532
+ let lo = 0
533
+ let hi = starts.length - 1
534
+ while (lo < hi) {
535
+ const mid = (lo + hi + 1) >> 1
536
+ if ((starts[mid] ?? 0) <= offset) lo = mid
537
+ else hi = mid - 1
538
+ }
539
+ return lo
540
+ }
541
+ regex.lastIndex = 0
542
+ for (let m = regex.exec(content); m !== null; m = regex.exec(content)) {
543
+ const text = m[0]
544
+ if (text.length === 0) {
545
+ regex.lastIndex++
546
+ continue
547
+ }
548
+ // A match that ends on a newline covers the lines before it, not the next one.
549
+ const end = m.index + text.length - (text.endsWith('\n') ? 1 : 0)
550
+ hits.push([lineOf(m.index), lineOf(Math.max(m.index, end))])
551
+ }
552
+ return hits
553
+ }
554
+
555
+ /** Lazy fragments: a dense file never allocates every match before paging. */
556
+ function* matchingFragments(content: string, regex: RegExp, multiline: boolean) {
557
+ const subjects = multiline ? [content] : content.split('\n')
558
+ let line = 1
559
+ for (const subject of subjects) {
560
+ regex.lastIndex = 0
561
+ let previous = 0
562
+ for (let match = regex.exec(subject); match !== null; match = regex.exec(subject)) {
563
+ if (multiline) {
564
+ for (let i = previous; i < match.index; i++) if (subject[i] === '\n') line++
565
+ previous = match.index
566
+ }
567
+ if (match[0].length === 0) {
568
+ regex.lastIndex++
569
+ continue
570
+ }
571
+ // Keep a multiline substring on one output row, never forge path rows.
572
+ yield { line, text: match[0].replace(/\n/g, '\\n') }
573
+ }
574
+ if (!multiline) line++
575
+ }
576
+ }
@@ -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.`,