@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.
- package/CHANGELOG.md +62 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +3 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/agents/file-definitions.d.ts +35 -4
- package/dist/agents/file-definitions.d.ts.map +1 -1
- package/dist/agents/file-definitions.js +98 -8
- package/dist/agents/file-definitions.js.map +1 -1
- package/dist/authorization/protected-paths.d.ts +9 -0
- package/dist/authorization/protected-paths.d.ts.map +1 -1
- package/dist/authorization/protected-paths.js +29 -3
- package/dist/authorization/protected-paths.js.map +1 -1
- package/dist/compaction/index.d.ts +1 -0
- package/dist/compaction/index.d.ts.map +1 -1
- package/dist/compaction/index.js.map +1 -1
- package/dist/compaction/manual.d.ts +7 -0
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +31 -12
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/model-summary.d.ts +101 -0
- package/dist/compaction/model-summary.d.ts.map +1 -0
- package/dist/compaction/model-summary.js +333 -0
- package/dist/compaction/model-summary.js.map +1 -0
- package/dist/compaction/verifier.d.ts +6 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +3 -3
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/config/runtime.d.ts +62 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +16 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +8 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +75 -2
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +2 -0
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +2 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/constants/plugin/index.d.ts +2 -0
- package/dist/constants/plugin/index.d.ts.map +1 -1
- package/dist/constants/plugin/index.js +2 -0
- package/dist/constants/plugin/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +25 -4
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +2 -1
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/persona/assembler.d.ts.map +1 -1
- package/dist/persona/assembler.js +2 -2
- package/dist/persona/assembler.js.map +1 -1
- package/dist/plugin/index.d.ts +1 -1
- package/dist/plugin/index.d.ts.map +1 -1
- package/dist/plugin/lifecycle.d.ts +8 -1
- package/dist/plugin/lifecycle.d.ts.map +1 -1
- package/dist/plugin/lifecycle.js +9 -2
- package/dist/plugin/lifecycle.js.map +1 -1
- package/dist/plugin/shell-hook-http.d.ts +39 -0
- package/dist/plugin/shell-hook-http.d.ts.map +1 -0
- package/dist/plugin/shell-hook-http.js +163 -0
- package/dist/plugin/shell-hook-http.js.map +1 -0
- package/dist/plugin/shell-hook.d.ts +93 -11
- package/dist/plugin/shell-hook.d.ts.map +1 -1
- package/dist/plugin/shell-hook.js +223 -39
- package/dist/plugin/shell-hook.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +2 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -2
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +2 -2
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +2 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -1
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/executor.d.ts +4 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +47 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +8 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +29 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +6 -2
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +18 -8
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +33 -9
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +52 -5
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +70 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/path-escalation.d.ts +2 -0
- package/dist/runtime/query/path-escalation.d.ts.map +1 -1
- package/dist/runtime/query/path-escalation.js +7 -4
- package/dist/runtime/query/path-escalation.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +2 -2
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts +2 -0
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +25 -0
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +3 -2
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +32 -13
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/scheduler/local.js +1 -1
- package/dist/scheduler/local.js.map +1 -1
- package/dist/session/retention/archive.js +1 -1
- package/dist/session/retention/archive.js.map +1 -1
- package/dist/session/workspace/driver.d.ts +6 -0
- package/dist/session/workspace/driver.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +28 -0
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/index.d.ts.map +1 -1
- package/dist/skills/index.js +1 -1
- package/dist/skills/index.js.map +1 -1
- package/dist/skills/loader.d.ts +7 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +53 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +2 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts +52 -0
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -0
- package/dist/tools/builtins/bash-cwd.js +127 -0
- package/dist/tools/builtins/bash-cwd.js.map +1 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +41 -6
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/bounded-file-read.d.ts +10 -0
- package/dist/tools/builtins/bounded-file-read.d.ts.map +1 -0
- package/dist/tools/builtins/bounded-file-read.js +67 -0
- package/dist/tools/builtins/bounded-file-read.js.map +1 -0
- package/dist/tools/builtins/grep.d.ts +6 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +187 -24
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +1 -0
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +113 -28
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-media.d.ts +68 -0
- package/dist/tools/builtins/read-media.d.ts.map +1 -0
- package/dist/tools/builtins/read-media.js +397 -0
- package/dist/tools/builtins/read-media.js.map +1 -0
- package/dist/tools/builtins/search-tools.js +1 -1
- package/dist/tools/builtins/search-tools.js.map +1 -1
- package/dist/tools/builtins/skill.d.ts +4 -2
- package/dist/tools/builtins/skill.d.ts.map +1 -1
- package/dist/tools/builtins/skill.js +61 -2
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/sleep-guard.d.ts +15 -0
- package/dist/tools/builtins/sleep-guard.d.ts.map +1 -0
- package/dist/tools/builtins/sleep-guard.js +33 -0
- package/dist/tools/builtins/sleep-guard.js.map +1 -0
- package/dist/tools/builtins/view-image.d.ts +23 -1
- package/dist/tools/builtins/view-image.d.ts.map +1 -1
- package/dist/tools/builtins/view-image.js +66 -58
- package/dist/tools/builtins/view-image.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +31 -0
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/toolsets/manager.js +3 -3
- package/dist/toolsets/manager.js.map +1 -1
- package/dist/types/agent/base.d.ts +8 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/agent/scheduler.d.ts +8 -0
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +6 -2
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/agent/task.js.map +1 -1
- package/dist/types/connector/mcp.d.ts +46 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +1 -1
- package/dist/types/plugin/index.d.ts +74 -7
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +12 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/session/sub-session.d.ts +6 -2
- package/dist/types/session/sub-session.d.ts.map +1 -1
- package/dist/types/skills/index.d.ts +36 -0
- package/dist/types/skills/index.d.ts.map +1 -1
- package/dist/types/skills/index.js +9 -0
- package/dist/types/skills/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +37 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/workspace/ref.d.ts +2 -0
- package/dist/types/workspace/ref.d.ts.map +1 -1
- package/dist/utils/frontmatter.d.ts +12 -0
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +6 -0
- package/dist/utils/frontmatter.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +3 -0
- package/src/agents/file-definitions.ts +126 -6
- package/src/authorization/protected-paths.ts +30 -2
- package/src/compaction/index.ts +1 -0
- package/src/compaction/manual.ts +68 -30
- package/src/compaction/model-summary.ts +412 -0
- package/src/compaction/verifier.ts +3 -3
- package/src/config/runtime.ts +16 -0
- package/src/connector/mcp/client.ts +87 -1
- package/src/constants/mcp/index.ts +3 -0
- package/src/constants/plugin/index.ts +2 -0
- package/src/manager/agent/lifecycle.ts +24 -5
- package/src/manager/session/turn-recorder.ts +2 -1
- package/src/persona/assembler.ts +2 -2
- package/src/plugin/index.ts +5 -0
- package/src/plugin/lifecycle.ts +16 -2
- package/src/plugin/shell-hook-http.ts +181 -0
- package/src/plugin/shell-hook.ts +386 -89
- package/src/prompt/coding-agent-doctrine.ts +2 -1
- package/src/public-runtime.ts +6 -0
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +5 -0
- package/src/runtime/query/executor.ts +54 -10
- package/src/runtime/query/index.ts +44 -0
- package/src/runtime/query/iteration/index.ts +11 -3
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +38 -9
- package/src/runtime/query/iteration/phases/compaction.ts +67 -5
- package/src/runtime/query/iteration/phases/context.ts +6 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +74 -3
- package/src/runtime/query/path-escalation.ts +11 -5
- package/src/runtime/query/plugin-hooks.ts +2 -2
- package/src/runtime/query/prepare-turn.ts +33 -0
- package/src/runtime/query/review-policy.ts +45 -16
- package/src/scheduler/local.ts +1 -1
- package/src/session/retention/archive.ts +1 -1
- package/src/session/workspace/driver.ts +6 -0
- package/src/session/workspace/git-worktree.ts +31 -0
- package/src/skills/index.ts +6 -1
- package/src/skills/loader.ts +54 -0
- package/src/skills/registry.ts +7 -6
- package/src/tools/builtins/bash-cwd.ts +156 -0
- package/src/tools/builtins/bash.ts +79 -34
- package/src/tools/builtins/bounded-file-read.ts +80 -0
- package/src/tools/builtins/grep.ts +198 -28
- package/src/tools/builtins/read-file.ts +151 -30
- package/src/tools/builtins/read-media.ts +483 -0
- package/src/tools/builtins/search-tools.ts +1 -1
- package/src/tools/builtins/skill.ts +91 -2
- package/src/tools/builtins/sleep-guard.ts +31 -0
- package/src/tools/builtins/view-image.ts +74 -62
- package/src/tools/file-read-tracker.ts +30 -0
- package/src/toolsets/manager.ts +3 -3
- package/src/types/agent/base.ts +9 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/agent/scheduler.ts +9 -0
- package/src/types/agent/task.ts +6 -2
- package/src/types/connector/mcp.ts +48 -0
- package/src/types/hitl/index.ts +1 -1
- package/src/types/plugin/index.ts +102 -16
- package/src/types/session/sub-session.ts +6 -2
- package/src/types/skills/index.ts +48 -0
- package/src/types/tool/index.ts +38 -0
- package/src/types/workspace/ref.ts +2 -0
- package/src/utils/frontmatter.ts +21 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
-
?
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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')
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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 {
|
|
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
|
|
82
|
-
const
|
|
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
|
|
132
|
-
|
|
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
|
|
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.`,
|