saeeol 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/cli/cmd/tui/component/dialog/dialog-agent.tsx +32 -0
- package/src/cli/cmd/tui/component/dialog/dialog-command.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-go-upsell.tsx +159 -0
- package/src/cli/cmd/tui/component/dialog/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog/dialog-model.tsx +238 -0
- package/src/cli/cmd/tui/component/dialog/dialog-provider.tsx +343 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-delete-failed.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-list.tsx +301 -0
- package/src/cli/cmd/tui/component/dialog/dialog-session-rename.tsx +35 -0
- package/src/cli/cmd/tui/component/dialog/dialog-skill.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog/dialog-status.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-create.tsx +200 -0
- package/src/cli/cmd/tui/component/dialog/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +1 -32
- package/src/cli/cmd/tui/component/dialog-command.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +1 -159
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +1 -86
- package/src/cli/cmd/tui/component/dialog-model.tsx +1 -238
- package/src/cli/cmd/tui/component/dialog-provider.tsx +1 -343
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +1 -103
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +1 -301
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +1 -35
- package/src/cli/cmd/tui/component/dialog-skill.tsx +1 -37
- package/src/cli/cmd/tui/component/dialog-stash.tsx +1 -87
- package/src/cli/cmd/tui/component/dialog-status.tsx +1 -190
- package/src/cli/cmd/tui/component/dialog-tag.tsx +1 -44
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +1 -50
- package/src/cli/cmd/tui/component/dialog-variant.tsx +1 -39
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +1 -200
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +1 -81
- package/src/session/compaction-helpers.ts +1 -169
- package/src/session/compaction.ts +1 -712
- package/src/session/core/compaction/compaction-helpers.ts +169 -0
- package/src/session/core/compaction/compaction.ts +712 -0
- package/src/session/core/compaction/overflow.ts +28 -0
- package/src/session/core/instruction.ts +234 -0
- package/src/session/core/llm.ts +504 -0
- package/src/session/core/network.ts +392 -0
- package/src/session/core/processor.ts +731 -0
- package/src/session/core/projectors.ts +139 -0
- package/src/session/core/resolve-tools.ts +241 -0
- package/src/session/core/retry.ts +149 -0
- package/src/session/core/revert.ts +173 -0
- package/src/session/core/run-state.ts +110 -0
- package/src/session/core/schema.ts +35 -0
- package/src/session/core/session-types.ts +160 -0
- package/src/session/core/session.sql.ts +124 -0
- package/src/session/core/session.ts +948 -0
- package/src/session/core/shell-exec.ts +205 -0
- package/src/session/core/status.ts +100 -0
- package/src/session/core/subtask.ts +268 -0
- package/src/session/core/summary.ts +173 -0
- package/src/session/core/system.ts +114 -0
- package/src/session/core/todo.ts +86 -0
- package/src/session/core/user-part.ts +293 -0
- package/src/session/instruction.ts +1 -234
- package/src/session/llm.ts +1 -504
- package/src/session/message/message-errors.ts +83 -0
- package/src/session/message/message-parts.ts +89 -0
- package/src/session/message/message-query.ts +107 -0
- package/src/session/message/message-transform.ts +156 -0
- package/src/session/message/message-types.ts +68 -0
- package/src/session/message/message-v2.ts +73 -0
- package/src/session/message/message.ts +192 -0
- package/src/session/message-errors.ts +1 -83
- package/src/session/message-parts.ts +1 -89
- package/src/session/message-query.ts +1 -107
- package/src/session/message-transform.ts +1 -156
- package/src/session/message-types.ts +1 -68
- package/src/session/message-v2.ts +1 -73
- package/src/session/message.ts +1 -192
- package/src/session/network.ts +1 -392
- package/src/session/overflow.ts +1 -28
- package/src/session/processor.ts +1 -731
- package/src/session/projectors.ts +2 -139
- package/src/session/prompt/prompt-command.ts +93 -0
- package/src/session/prompt/prompt-loop.ts +299 -0
- package/src/session/prompt/prompt-model.ts +44 -0
- package/src/session/prompt/prompt-reminders.ts +120 -0
- package/src/session/prompt/prompt-resolve.ts +42 -0
- package/src/session/prompt/prompt-schemas.ts +128 -0
- package/src/session/prompt/prompt-title.ts +55 -0
- package/src/session/prompt/prompt-types.ts +47 -0
- package/src/session/prompt/prompt-user-msg.ts +80 -0
- package/src/session/prompt/prompt.ts +211 -0
- package/src/session/prompt-command.ts +1 -93
- package/src/session/prompt-loop.ts +1 -299
- package/src/session/prompt-model.ts +1 -44
- package/src/session/prompt-reminders.ts +1 -120
- package/src/session/prompt-resolve.ts +1 -42
- package/src/session/prompt-schemas.ts +1 -128
- package/src/session/prompt-title.ts +1 -55
- package/src/session/prompt-types.ts +1 -47
- package/src/session/prompt-user-msg.ts +1 -80
- package/src/session/prompt.ts +1 -211
- package/src/session/resolve-tools.ts +1 -241
- package/src/session/retry.ts +1 -149
- package/src/session/revert.ts +1 -173
- package/src/session/run-state.ts +1 -110
- package/src/session/schema.ts +1 -35
- package/src/session/session-types.ts +1 -160
- package/src/session/session.sql.ts +1 -124
- package/src/session/session.ts +1 -948
- package/src/session/shell-exec.ts +1 -205
- package/src/session/status.ts +1 -100
- package/src/session/subtask.ts +1 -268
- package/src/session/summary.ts +1 -173
- package/src/session/system.ts +1 -114
- package/src/session/todo.ts +1 -86
- package/src/session/user-part.ts +1 -293
- package/src/tool/apply_patch.ts +1 -334
- package/src/tool/bash.ts +1 -656
- package/src/tool/core/external-directory.ts +55 -0
- package/src/tool/core/invalid.ts +21 -0
- package/src/tool/core/recall.ts +164 -0
- package/src/tool/core/recall.txt +12 -0
- package/src/tool/core/schema.ts +16 -0
- package/src/tool/core/tool.ts +162 -0
- package/src/tool/core/truncate.ts +160 -0
- package/src/tool/core/truncation-dir.ts +4 -0
- package/src/tool/diagnostics.ts +1 -20
- package/src/tool/edit-replacers.ts +1 -288
- package/src/tool/edit-utils.ts +1 -86
- package/src/tool/edit.ts +1 -262
- package/src/tool/external-directory.ts +1 -55
- package/src/tool/file/apply_patch.ts +334 -0
- package/src/tool/file/apply_patch.txt +33 -0
- package/src/tool/file/bash.ts +656 -0
- package/src/tool/file/bash.txt +119 -0
- package/src/tool/file/edit-replacers.ts +288 -0
- package/src/tool/file/edit-utils.ts +86 -0
- package/src/tool/file/edit.ts +262 -0
- package/src/tool/file/edit.txt +10 -0
- package/src/tool/file/read.ts +389 -0
- package/src/tool/file/read.txt +14 -0
- package/src/tool/file/write.ts +114 -0
- package/src/tool/file/write.txt +8 -0
- package/src/tool/glob.ts +1 -115
- package/src/tool/grep.ts +1 -151
- package/src/tool/integration/diagnostics.ts +20 -0
- package/src/tool/integration/lsp.ts +113 -0
- package/src/tool/integration/lsp.txt +24 -0
- package/src/tool/integration/mcp-exa.ts +73 -0
- package/src/tool/integration/package.ts +168 -0
- package/src/tool/integration/registry.ts +375 -0
- package/src/tool/invalid.ts +1 -21
- package/src/tool/lsp.ts +1 -113
- package/src/tool/mcp-exa.ts +1 -73
- package/src/tool/package.ts +1 -168
- package/src/tool/plan.ts +1 -30
- package/src/tool/question.ts +1 -52
- package/src/tool/read.ts +1 -389
- package/src/tool/recall.ts +1 -164
- package/src/tool/registry.ts +1 -375
- package/src/tool/schema.ts +1 -16
- package/src/tool/search/glob.ts +115 -0
- package/src/tool/search/glob.txt +6 -0
- package/src/tool/search/grep.ts +151 -0
- package/src/tool/search/grep.txt +8 -0
- package/src/tool/search/warpgrep.ts +107 -0
- package/src/tool/search/warpgrep.txt +10 -0
- package/src/tool/search/webfetch.ts +202 -0
- package/src/tool/search/webfetch.txt +13 -0
- package/src/tool/search/websearch.ts +71 -0
- package/src/tool/search/websearch.txt +14 -0
- package/src/tool/skill.ts +1 -91
- package/src/tool/task.ts +1 -197
- package/src/tool/todo.ts +1 -62
- package/src/tool/tool.ts +1 -162
- package/src/tool/truncate.ts +1 -160
- package/src/tool/truncation-dir.ts +1 -4
- package/src/tool/warpgrep.ts +1 -107
- package/src/tool/webfetch.ts +1 -202
- package/src/tool/websearch.ts +1 -71
- package/src/tool/workflow/plan-enter.txt +14 -0
- package/src/tool/workflow/plan-exit.txt +13 -0
- package/src/tool/workflow/plan.ts +30 -0
- package/src/tool/workflow/question.ts +52 -0
- package/src/tool/workflow/question.txt +11 -0
- package/src/tool/workflow/skill.ts +91 -0
- package/src/tool/workflow/skill.txt +5 -0
- package/src/tool/workflow/task.ts +197 -0
- package/src/tool/workflow/task.txt +57 -0
- package/src/tool/workflow/todo.ts +62 -0
- package/src/tool/workflow/todowrite.txt +167 -0
- package/src/tool/write.ts +1 -114
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// the approaches in this edit tool are sourced from
|
|
2
|
+
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
|
3
|
+
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
|
4
|
+
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
|
|
5
|
+
|
|
6
|
+
import * as path from "path"
|
|
7
|
+
import { Effect, Schema, Semaphore } from "effect"
|
|
8
|
+
import * as Tool from "../core/tool"
|
|
9
|
+
import { LSP } from "@/lsp/lsp"
|
|
10
|
+
import { createTwoFilesPatch } from "diff"
|
|
11
|
+
import DESCRIPTION from "./edit.txt"
|
|
12
|
+
import { File } from "../../file"
|
|
13
|
+
import { FileWatcher } from "../../file/watcher"
|
|
14
|
+
import { Bus } from "../../bus"
|
|
15
|
+
import { Format } from "../../format"
|
|
16
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
17
|
+
import { Snapshot } from "@/snapshot"
|
|
18
|
+
import { assertExternalDirectoryEffect } from "../core/external-directory"
|
|
19
|
+
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
20
|
+
import * as Bom from "@/util/bom"
|
|
21
|
+
import { filterDiagnostics } from "../integration/diagnostics"
|
|
22
|
+
import { ConfigValidation } from "../../overlay/config-validation"
|
|
23
|
+
import * as EncodedIO from "../../overlay/tool/encoded-io"
|
|
24
|
+
import { buildFileDiff, normalizeLineEndings, detectLineEnding, convertToLineEnding, trimDiff } from "./edit-utils"
|
|
25
|
+
import {
|
|
26
|
+
SimpleReplacer,
|
|
27
|
+
LineTrimmedReplacer,
|
|
28
|
+
BlockAnchorReplacer,
|
|
29
|
+
WhitespaceNormalizedReplacer,
|
|
30
|
+
IndentationFlexibleReplacer,
|
|
31
|
+
EscapeNormalizedReplacer,
|
|
32
|
+
MultiOccurrenceReplacer,
|
|
33
|
+
TrimmedBoundaryReplacer,
|
|
34
|
+
ContextAwareReplacer,
|
|
35
|
+
} from "./edit-replacers"
|
|
36
|
+
|
|
37
|
+
export { buildFileDiff, trimDiff } from "./edit-utils"
|
|
38
|
+
export type { Replacer } from "./edit-replacers"
|
|
39
|
+
export {
|
|
40
|
+
SimpleReplacer,
|
|
41
|
+
LineTrimmedReplacer,
|
|
42
|
+
BlockAnchorReplacer,
|
|
43
|
+
WhitespaceNormalizedReplacer,
|
|
44
|
+
IndentationFlexibleReplacer,
|
|
45
|
+
EscapeNormalizedReplacer,
|
|
46
|
+
MultiOccurrenceReplacer,
|
|
47
|
+
TrimmedBoundaryReplacer,
|
|
48
|
+
ContextAwareReplacer,
|
|
49
|
+
} from "./edit-replacers"
|
|
50
|
+
|
|
51
|
+
const locks = new Map<string, Semaphore.Semaphore>()
|
|
52
|
+
|
|
53
|
+
function lock(filePath: string) {
|
|
54
|
+
const resolvedFilePath = AppFileSystem.resolve(filePath)
|
|
55
|
+
const hit = locks.get(resolvedFilePath)
|
|
56
|
+
if (hit) return hit
|
|
57
|
+
|
|
58
|
+
const next = Semaphore.makeUnsafe(1)
|
|
59
|
+
locks.set(resolvedFilePath, next)
|
|
60
|
+
return next
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const Parameters = Schema.Struct({
|
|
64
|
+
filePath: Schema.String.annotate({ description: "The absolute path to the file to modify" }),
|
|
65
|
+
oldString: Schema.String.annotate({ description: "The text to replace" }),
|
|
66
|
+
newString: Schema.String.annotate({
|
|
67
|
+
description: "The text to replace it with (must be different from oldString)",
|
|
68
|
+
}),
|
|
69
|
+
replaceAll: Schema.optional(Schema.Boolean).annotate({
|
|
70
|
+
description: "Replace all occurrences of oldString (default false)",
|
|
71
|
+
}),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export function replace(content: string, oldString: string, newString: string, replaceAll = false): string {
|
|
75
|
+
if (oldString === newString) {
|
|
76
|
+
throw new Error("No changes to apply: oldString and newString are identical.")
|
|
77
|
+
}
|
|
78
|
+
let notFound = true
|
|
79
|
+
for (const replacer of [
|
|
80
|
+
SimpleReplacer,
|
|
81
|
+
LineTrimmedReplacer,
|
|
82
|
+
BlockAnchorReplacer,
|
|
83
|
+
WhitespaceNormalizedReplacer,
|
|
84
|
+
IndentationFlexibleReplacer,
|
|
85
|
+
EscapeNormalizedReplacer,
|
|
86
|
+
TrimmedBoundaryReplacer,
|
|
87
|
+
ContextAwareReplacer,
|
|
88
|
+
MultiOccurrenceReplacer,
|
|
89
|
+
]) {
|
|
90
|
+
for (const search of replacer(content, oldString)) {
|
|
91
|
+
const index = content.indexOf(search)
|
|
92
|
+
if (index === -1) continue
|
|
93
|
+
notFound = false
|
|
94
|
+
if (replaceAll) {
|
|
95
|
+
return content.replaceAll(search, newString)
|
|
96
|
+
}
|
|
97
|
+
const lastIndex = content.lastIndexOf(search)
|
|
98
|
+
if (index !== lastIndex) continue
|
|
99
|
+
return content.substring(0, index) + newString + content.substring(index + search.length)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (notFound) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"Could not find oldString in the file. It must match exactly, including whitespace, indentation, and line endings.",
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
throw new Error("Found multiple matches for oldString. Provide more surrounding context to make the match unique.")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const EditTool = Tool.define(
|
|
111
|
+
"edit",
|
|
112
|
+
Effect.gen(function* () {
|
|
113
|
+
const lsp = yield* LSP.Service
|
|
114
|
+
const afs = yield* AppFileSystem.Service
|
|
115
|
+
const format = yield* Format.Service
|
|
116
|
+
const bus = yield* Bus.Service
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
description: DESCRIPTION,
|
|
120
|
+
parameters: Parameters,
|
|
121
|
+
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
|
122
|
+
Effect.gen(function* () {
|
|
123
|
+
if (!params.filePath) {
|
|
124
|
+
throw new Error("filePath is required")
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (params.oldString === params.newString) {
|
|
128
|
+
throw new Error("No changes to apply: oldString and newString are identical.")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const instance = yield* InstanceState.context
|
|
132
|
+
const filePath = path.isAbsolute(params.filePath)
|
|
133
|
+
? params.filePath
|
|
134
|
+
: path.join(instance.directory, params.filePath)
|
|
135
|
+
yield* assertExternalDirectoryEffect(ctx, filePath)
|
|
136
|
+
|
|
137
|
+
let diff = ""
|
|
138
|
+
let contentOld = ""
|
|
139
|
+
let contentNew = ""
|
|
140
|
+
let cachedFilediff: Snapshot.FileDiff | undefined
|
|
141
|
+
yield* lock(filePath).withPermits(1)(
|
|
142
|
+
Effect.gen(function* () {
|
|
143
|
+
if (params.oldString === "") {
|
|
144
|
+
const existed = yield* afs.existsSafe(filePath)
|
|
145
|
+
// derive the BOM flag from the detected encoding label instead of the decoded text.
|
|
146
|
+
const pre = existed ? yield* EncodedIO.read(filePath) : { text: "", encoding: "utf-8" }
|
|
147
|
+
const source = { bom: pre.encoding === "utf-8-bom", text: pre.text, encoding: pre.encoding }
|
|
148
|
+
const next = Bom.split(params.newString)
|
|
149
|
+
const desiredBom = source.bom || next.bom
|
|
150
|
+
contentOld = source.text
|
|
151
|
+
contentNew = next.text
|
|
152
|
+
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
|
|
153
|
+
cachedFilediff = buildFileDiff(filePath, contentOld, contentNew)
|
|
154
|
+
yield* ctx.ask({
|
|
155
|
+
permission: "edit",
|
|
156
|
+
patterns: [path.relative(instance.worktree, filePath)],
|
|
157
|
+
always: ["*"],
|
|
158
|
+
metadata: {
|
|
159
|
+
filepath: filePath,
|
|
160
|
+
diff,
|
|
161
|
+
filediff: cachedFilediff,
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
yield* EncodedIO.write(filePath, Bom.join(contentNew, desiredBom), source.encoding)
|
|
165
|
+
if (yield* format.file(filePath)) {
|
|
166
|
+
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
|
|
167
|
+
}
|
|
168
|
+
yield* bus.publish(File.Event.Edited, { file: filePath })
|
|
169
|
+
yield* bus.publish(FileWatcher.Event.Updated, {
|
|
170
|
+
file: filePath,
|
|
171
|
+
event: existed ? "change" : "add",
|
|
172
|
+
})
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const info = yield* afs.stat(filePath).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
177
|
+
if (!info) throw new Error(`File ${filePath} not found`)
|
|
178
|
+
if (info.type === "Directory") throw new Error(`Path is a directory, not a file: ${filePath}`)
|
|
179
|
+
// derive the BOM flag from the detected encoding label instead of the decoded text.
|
|
180
|
+
const pre = yield* EncodedIO.read(filePath)
|
|
181
|
+
const source = { bom: pre.encoding === "utf-8-bom", text: pre.text, encoding: pre.encoding }
|
|
182
|
+
contentOld = source.text
|
|
183
|
+
|
|
184
|
+
const ending = detectLineEnding(contentOld)
|
|
185
|
+
const old = convertToLineEnding(normalizeLineEndings(params.oldString), ending)
|
|
186
|
+
const replacement = convertToLineEnding(normalizeLineEndings(params.newString), ending)
|
|
187
|
+
|
|
188
|
+
const next = Bom.split(replace(contentOld, old, replacement, params.replaceAll))
|
|
189
|
+
const desiredBom = source.bom || next.bom
|
|
190
|
+
contentNew = next.text
|
|
191
|
+
|
|
192
|
+
diff = trimDiff(
|
|
193
|
+
createTwoFilesPatch(
|
|
194
|
+
filePath,
|
|
195
|
+
filePath,
|
|
196
|
+
normalizeLineEndings(contentOld),
|
|
197
|
+
normalizeLineEndings(contentNew),
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
cachedFilediff = buildFileDiff(filePath, contentOld, contentNew)
|
|
201
|
+
yield* ctx.ask({
|
|
202
|
+
permission: "edit",
|
|
203
|
+
patterns: [path.relative(instance.worktree, filePath)],
|
|
204
|
+
always: ["*"],
|
|
205
|
+
metadata: {
|
|
206
|
+
filepath: filePath,
|
|
207
|
+
diff,
|
|
208
|
+
filediff: cachedFilediff,
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
yield* EncodedIO.write(filePath, Bom.join(contentNew, desiredBom), source.encoding)
|
|
213
|
+
if (yield* format.file(filePath)) {
|
|
214
|
+
contentNew = yield* Bom.syncFile(afs, filePath, desiredBom)
|
|
215
|
+
}
|
|
216
|
+
yield* bus.publish(File.Event.Edited, { file: filePath })
|
|
217
|
+
yield* bus.publish(FileWatcher.Event.Updated, {
|
|
218
|
+
file: filePath,
|
|
219
|
+
event: "change",
|
|
220
|
+
})
|
|
221
|
+
diff = trimDiff(
|
|
222
|
+
createTwoFilesPatch(
|
|
223
|
+
filePath,
|
|
224
|
+
filePath,
|
|
225
|
+
normalizeLineEndings(contentOld),
|
|
226
|
+
normalizeLineEndings(contentNew),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
}).pipe(Effect.orDie),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const filediff: Snapshot.FileDiff = cachedFilediff ?? buildFileDiff(filePath, contentOld, contentNew)
|
|
233
|
+
|
|
234
|
+
yield* ctx.metadata({
|
|
235
|
+
metadata: {
|
|
236
|
+
diff,
|
|
237
|
+
filediff,
|
|
238
|
+
diagnostics: {},
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
let output = "Edit applied successfully."
|
|
243
|
+
yield* lsp.touchFile(filePath, "document")
|
|
244
|
+
const diagnostics = yield* lsp.diagnostics()
|
|
245
|
+
const normalizedFilePath = AppFileSystem.normalizePath(filePath)
|
|
246
|
+
const block = LSP.Diagnostic.report(filePath, diagnostics[normalizedFilePath] ?? [])
|
|
247
|
+
if (block) output += `\n\nLSP errors detected in this file, please fix:\n${block}`
|
|
248
|
+
output += yield* Effect.promise(() => ConfigValidation.check(filePath))
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
metadata: {
|
|
252
|
+
diagnostics: filterDiagnostics(diagnostics, [normalizedFilePath]),
|
|
253
|
+
diff,
|
|
254
|
+
filediff,
|
|
255
|
+
},
|
|
256
|
+
title: `${path.relative(instance.worktree, filePath)}`,
|
|
257
|
+
output,
|
|
258
|
+
}
|
|
259
|
+
}),
|
|
260
|
+
}
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Performs exact string replacements in files.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
5
|
+
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
|
|
6
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
7
|
+
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
8
|
+
- The edit will FAIL if `oldString` is not found in the file with an error "oldString not found in content".
|
|
9
|
+
- The edit will FAIL if `oldString` is found multiple times in the file with an error "Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match." Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`.
|
|
10
|
+
- Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { lstat } from "fs/promises"
|
|
2
|
+
import { Effect, Option, Schema, Scope } from "effect"
|
|
3
|
+
import { NonNegativeInt } from "@/util/schema"
|
|
4
|
+
import * as path from "path"
|
|
5
|
+
import type { Readable } from "stream"
|
|
6
|
+
import { createInterface } from "readline"
|
|
7
|
+
import * as Tool from "../core/tool"
|
|
8
|
+
import { AppFileSystem } from "@saeeol/core/filesystem"
|
|
9
|
+
import { LSP } from "@/lsp/lsp"
|
|
10
|
+
import DESCRIPTION from "./read.txt"
|
|
11
|
+
import { InstanceState } from "@/effect/instance-state"
|
|
12
|
+
import { assertExternalDirectoryEffect } from "../core/external-directory"
|
|
13
|
+
import { Instruction } from "../../session/instruction"
|
|
14
|
+
import { isPdfAttachment, sniffAttachmentMime } from "@/util/media"
|
|
15
|
+
import * as Encoding from "../../overlay/encoding"
|
|
16
|
+
import * as TextStream from "../../overlay/text-stream"
|
|
17
|
+
|
|
18
|
+
const DEFAULT_READ_LIMIT = 2000
|
|
19
|
+
const MAX_LINE_LENGTH = 2000
|
|
20
|
+
const MAX_LINE_SUFFIX = `... (line truncated to ${MAX_LINE_LENGTH} chars)`
|
|
21
|
+
const MAX_BYTES = 50 * 1024
|
|
22
|
+
const MAX_BYTES_LABEL = `${MAX_BYTES / 1024} KB`
|
|
23
|
+
const SAMPLE_BYTES = 4096
|
|
24
|
+
const DIRECTORY_CONCURRENCY = 8
|
|
25
|
+
const SUPPORTED_IMAGE_MIMES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"])
|
|
26
|
+
|
|
27
|
+
// `offset` and `limit` were originally `z.coerce.number()` — the runtime
|
|
28
|
+
// coercion was useful when the tool was called from a shell but serves no
|
|
29
|
+
// purpose in the LLM tool-call path (the model emits typed JSON). The JSON
|
|
30
|
+
// Schema output is identical (`type: "number"`), so the LLM view is
|
|
31
|
+
// unchanged; purely CLI-facing uses must now send numbers rather than strings.
|
|
32
|
+
export const Parameters = Schema.Struct({
|
|
33
|
+
filePath: Schema.String.annotate({ description: "The absolute path to the file or directory to read" }),
|
|
34
|
+
offset: Schema.optional(NonNegativeInt).annotate({
|
|
35
|
+
description: "The line number to start reading from (1-indexed)",
|
|
36
|
+
}),
|
|
37
|
+
limit: Schema.optional(NonNegativeInt).annotate({
|
|
38
|
+
description: "The maximum number of lines to read (defaults to 2000)",
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const ReadTool = Tool.define(
|
|
43
|
+
"read",
|
|
44
|
+
Effect.gen(function* () {
|
|
45
|
+
const fs = yield* AppFileSystem.Service
|
|
46
|
+
const instruction = yield* Instruction.Service
|
|
47
|
+
const lsp = yield* LSP.Service
|
|
48
|
+
const scope = yield* Scope.Scope
|
|
49
|
+
|
|
50
|
+
const miss = Effect.fn("ReadTool.miss")(function* (filepath: string) {
|
|
51
|
+
const dir = path.dirname(filepath)
|
|
52
|
+
const base = path.basename(filepath)
|
|
53
|
+
const items = yield* fs.readDirectory(dir).pipe(
|
|
54
|
+
Effect.map((items) =>
|
|
55
|
+
items
|
|
56
|
+
.filter(
|
|
57
|
+
(item) =>
|
|
58
|
+
item.toLowerCase().includes(base.toLowerCase()) || base.toLowerCase().includes(item.toLowerCase()),
|
|
59
|
+
)
|
|
60
|
+
.map((item) => path.join(dir, item))
|
|
61
|
+
.slice(0, 3),
|
|
62
|
+
),
|
|
63
|
+
Effect.catch(() => Effect.succeed([] as string[])),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if (items.length > 0) {
|
|
67
|
+
return yield* Effect.fail(
|
|
68
|
+
new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${items.join("\n")}`),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return yield* Effect.fail(new Error(`File not found: ${filepath}`))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const list = Effect.fn("ReadTool.list")(function* (filepath: string) {
|
|
76
|
+
const items = yield* fs.readDirectoryEntries(filepath)
|
|
77
|
+
return yield* Effect.forEach(
|
|
78
|
+
items,
|
|
79
|
+
Effect.fnUntraced(function* (item) {
|
|
80
|
+
if (item.type === "directory") return item.name + "/"
|
|
81
|
+
if (item.type !== "symlink") return item.name
|
|
82
|
+
|
|
83
|
+
const target = yield* fs.stat(path.join(filepath, item.name)).pipe(Effect.catch(() => Effect.void))
|
|
84
|
+
if (target?.type === "Directory") return item.name + "/"
|
|
85
|
+
return item.name
|
|
86
|
+
}),
|
|
87
|
+
{ concurrency: "unbounded" },
|
|
88
|
+
).pipe(Effect.map((items: string[]) => items.sort((a, b) => a.localeCompare(b))))
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const warm = Effect.fn("ReadTool.warm")(function* (filepath: string) {
|
|
92
|
+
yield* lsp.touchFile(filepath).pipe(Effect.ignore, Effect.forkIn(scope))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const readSample = Effect.fn("ReadTool.readSample")(function* (
|
|
96
|
+
filepath: string,
|
|
97
|
+
fileSize: number,
|
|
98
|
+
sampleSize: number,
|
|
99
|
+
) {
|
|
100
|
+
if (fileSize === 0) return new Uint8Array()
|
|
101
|
+
|
|
102
|
+
return yield* Effect.scoped(
|
|
103
|
+
Effect.gen(function* () {
|
|
104
|
+
const file = yield* fs.open(filepath, { flag: "r" })
|
|
105
|
+
return Option.getOrElse(yield* file.readAlloc(Math.min(sampleSize, fileSize)), () => new Uint8Array())
|
|
106
|
+
}),
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const isBinaryFile = (filepath: string, bytes: Uint8Array) => {
|
|
111
|
+
const ext = path.extname(filepath).toLowerCase()
|
|
112
|
+
switch (ext) {
|
|
113
|
+
case ".zip":
|
|
114
|
+
case ".tar":
|
|
115
|
+
case ".gz":
|
|
116
|
+
case ".exe":
|
|
117
|
+
case ".dll":
|
|
118
|
+
case ".so":
|
|
119
|
+
case ".class":
|
|
120
|
+
case ".jar":
|
|
121
|
+
case ".war":
|
|
122
|
+
case ".7z":
|
|
123
|
+
case ".doc":
|
|
124
|
+
case ".docx":
|
|
125
|
+
case ".xls":
|
|
126
|
+
case ".xlsx":
|
|
127
|
+
case ".ppt":
|
|
128
|
+
case ".pptx":
|
|
129
|
+
case ".odt":
|
|
130
|
+
case ".ods":
|
|
131
|
+
case ".odp":
|
|
132
|
+
case ".bin":
|
|
133
|
+
case ".dat":
|
|
134
|
+
case ".obj":
|
|
135
|
+
case ".o":
|
|
136
|
+
case ".a":
|
|
137
|
+
case ".lib":
|
|
138
|
+
case ".wasm":
|
|
139
|
+
case ".pyc":
|
|
140
|
+
case ".pyo":
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (bytes.length === 0) return false
|
|
145
|
+
const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
146
|
+
if (Encoding.hasUtf16Bom(buf, bytes.length) || Encoding.hasUtf32Bom(buf, bytes.length)) return false
|
|
147
|
+
|
|
148
|
+
let nonPrintableCount = 0
|
|
149
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
150
|
+
if (bytes[i] === 0) return true
|
|
151
|
+
if (bytes[i] < 9 || (bytes[i] > 13 && bytes[i] < 32)) {
|
|
152
|
+
nonPrintableCount++
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return nonPrintableCount / bytes.length > 0.3
|
|
157
|
+
}
|
|
158
|
+
type DirectoryFile = {
|
|
159
|
+
filepath: string
|
|
160
|
+
content: string
|
|
161
|
+
}
|
|
162
|
+
const readDirectoryFiles = Effect.fn("ReadTool.readDirectoryFiles")(function* (
|
|
163
|
+
filepath: string,
|
|
164
|
+
items: string[],
|
|
165
|
+
directory: string,
|
|
166
|
+
) {
|
|
167
|
+
const entries = yield* fs.readDirectoryEntries(filepath).pipe(Effect.catch(() => Effect.succeed([])))
|
|
168
|
+
const types = new Map(entries.map((entry) => [entry.name, entry.type]))
|
|
169
|
+
const files = yield* Effect.forEach(
|
|
170
|
+
items.filter((item) => !item.endsWith("/") && types.get(item) === "file"),
|
|
171
|
+
Effect.fnUntraced(function* (item) {
|
|
172
|
+
const child = path.join(filepath, item)
|
|
173
|
+
const info = yield* Effect.promise(() => lstat(child)).pipe(Effect.catch(() => Effect.void))
|
|
174
|
+
if (!info?.isFile()) return
|
|
175
|
+
const sample = yield* readSample(child, Number(info.size), SAMPLE_BYTES).pipe(
|
|
176
|
+
Effect.catch(() => Effect.succeed(new Uint8Array())),
|
|
177
|
+
)
|
|
178
|
+
if (isBinaryFile(child, sample)) return
|
|
179
|
+
const file = yield* Effect.promise(() => lines(child, { limit: DEFAULT_READ_LIMIT, offset: 1 })).pipe(
|
|
180
|
+
Effect.catch(() => Effect.void),
|
|
181
|
+
)
|
|
182
|
+
if (!file) return
|
|
183
|
+
const rel = path.relative(directory, child).replaceAll("\\", "/")
|
|
184
|
+
const note = file.cut || file.more ? "\n\n(File truncated)" : ""
|
|
185
|
+
return {
|
|
186
|
+
filepath: child,
|
|
187
|
+
content: `<file_content path="${rel}">\n${file.raw.join("\n")}${note}\n</file_content>`,
|
|
188
|
+
}
|
|
189
|
+
}),
|
|
190
|
+
{ concurrency: DIRECTORY_CONCURRENCY },
|
|
191
|
+
)
|
|
192
|
+
return files.filter((item): item is DirectoryFile => item !== undefined)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const run = Effect.fn("ReadTool.execute")(function* (
|
|
196
|
+
params: Schema.Schema.Type<typeof Parameters>,
|
|
197
|
+
ctx: Tool.Context,
|
|
198
|
+
) {
|
|
199
|
+
const instance = yield* InstanceState.context
|
|
200
|
+
let filepath = params.filePath
|
|
201
|
+
if (!path.isAbsolute(filepath)) {
|
|
202
|
+
filepath = path.resolve(instance.directory, filepath)
|
|
203
|
+
}
|
|
204
|
+
if (process.platform === "win32") {
|
|
205
|
+
filepath = AppFileSystem.normalizePath(filepath)
|
|
206
|
+
}
|
|
207
|
+
const title = path.relative(instance.worktree, filepath)
|
|
208
|
+
|
|
209
|
+
const stat = yield* fs.stat(filepath).pipe(
|
|
210
|
+
Effect.catchIf(
|
|
211
|
+
(err) => "reason" in err && err.reason._tag === "NotFound",
|
|
212
|
+
() => Effect.succeed(undefined),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
yield* assertExternalDirectoryEffect(ctx, filepath, {
|
|
217
|
+
bypass: Boolean(ctx.extra?.["bypassCwdCheck"]),
|
|
218
|
+
kind: stat?.type === "Directory" ? "directory" : "file",
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
yield* ctx.ask({
|
|
222
|
+
permission: "read",
|
|
223
|
+
patterns: [filepath],
|
|
224
|
+
always: ["*"],
|
|
225
|
+
metadata: {},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (!stat) return yield* miss(filepath)
|
|
229
|
+
|
|
230
|
+
if (stat.type === "Directory") {
|
|
231
|
+
const items = yield* list(filepath)
|
|
232
|
+
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
|
233
|
+
const offset = params.offset || 1
|
|
234
|
+
const start = offset - 1
|
|
235
|
+
const sliced = items.slice(start, start + limit)
|
|
236
|
+
const truncated = start + sliced.length < items.length
|
|
237
|
+
const expand = Boolean(ctx.extra?.["includeDirectoryFiles"])
|
|
238
|
+
const loaded = expand ? yield* readDirectoryFiles(filepath, sliced, instance.directory) : []
|
|
239
|
+
const content = loaded.map((item) => item.content).join("\n\n")
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
title,
|
|
243
|
+
output: [
|
|
244
|
+
`<path>${filepath}</path>`,
|
|
245
|
+
`<type>directory</type>`,
|
|
246
|
+
`<entries>`,
|
|
247
|
+
sliced.join("\n"),
|
|
248
|
+
truncated
|
|
249
|
+
? `\n(Showing ${sliced.length} of ${items.length} entries. Use 'offset' parameter to read beyond entry ${offset + sliced.length})`
|
|
250
|
+
: `\n(${items.length} entries)`,
|
|
251
|
+
`</entries>`,
|
|
252
|
+
...(content ? [`\n${content}`] : []),
|
|
253
|
+
].join("\n"),
|
|
254
|
+
metadata: {
|
|
255
|
+
preview: sliced.slice(0, 20).join("\n"),
|
|
256
|
+
truncated,
|
|
257
|
+
loaded: loaded.map((item) => item.filepath),
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const loaded = yield* instruction.resolve(ctx.messages, filepath, ctx.messageID)
|
|
263
|
+
const sample = yield* readSample(filepath, Number(stat.size), SAMPLE_BYTES)
|
|
264
|
+
|
|
265
|
+
const mime = sniffAttachmentMime(sample, AppFileSystem.mimeType(filepath))
|
|
266
|
+
const isImage = SUPPORTED_IMAGE_MIMES.has(mime)
|
|
267
|
+
|
|
268
|
+
if (isImage || isPdfAttachment(mime)) {
|
|
269
|
+
const bytes = yield* fs.readFile(filepath)
|
|
270
|
+
const msg = isPdfAttachment(mime) ? "PDF read successfully" : "Image read successfully"
|
|
271
|
+
return {
|
|
272
|
+
title,
|
|
273
|
+
output: msg,
|
|
274
|
+
metadata: {
|
|
275
|
+
preview: msg,
|
|
276
|
+
truncated: false,
|
|
277
|
+
loaded: loaded.map((item) => item.filepath),
|
|
278
|
+
},
|
|
279
|
+
attachments: [
|
|
280
|
+
{
|
|
281
|
+
type: "file" as const,
|
|
282
|
+
mime,
|
|
283
|
+
url: `data:${mime};base64,${Buffer.from(bytes).toString("base64")}`,
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (isBinaryFile(filepath, sample)) {
|
|
290
|
+
return yield* Effect.fail(new Error(`Cannot read binary file: ${filepath}`))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const file = yield* Effect.promise(() =>
|
|
294
|
+
lines(filepath, { limit: params.limit ?? DEFAULT_READ_LIMIT, offset: params.offset || 1 }),
|
|
295
|
+
)
|
|
296
|
+
if (file.count < file.offset && !(file.count === 0 && file.offset === 1)) {
|
|
297
|
+
return yield* Effect.fail(
|
|
298
|
+
new Error(`Offset ${file.offset} is out of range for this file (${file.count} lines)`),
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let output = [`<path>${filepath}</path>`, `<type>file</type>`, "<content>\n"].join("\n")
|
|
303
|
+
output += file.raw.map((line, i) => `${i + file.offset}: ${line}`).join("\n")
|
|
304
|
+
|
|
305
|
+
const last = file.offset + file.raw.length - 1
|
|
306
|
+
const next = last + 1
|
|
307
|
+
const truncated = file.more || file.cut
|
|
308
|
+
if (file.cut) {
|
|
309
|
+
output += `\n\n(Output capped at ${MAX_BYTES_LABEL}. Showing lines ${file.offset}-${last}. Use offset=${next} to continue.)`
|
|
310
|
+
} else if (file.more) {
|
|
311
|
+
output += `\n\n(Showing lines ${file.offset}-${last} of ${file.count}. Use offset=${next} to continue.)`
|
|
312
|
+
} else {
|
|
313
|
+
output += `\n\n(End of file - total ${file.count} lines)`
|
|
314
|
+
}
|
|
315
|
+
output += "\n</content>"
|
|
316
|
+
|
|
317
|
+
yield* warm(filepath)
|
|
318
|
+
|
|
319
|
+
if (loaded.length > 0) {
|
|
320
|
+
output += `\n\n<system-reminder>\n${loaded.map((item) => item.content).join("\n\n")}\n</system-reminder>`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
title,
|
|
325
|
+
output,
|
|
326
|
+
metadata: {
|
|
327
|
+
preview: file.raw.slice(0, 20).join("\n"),
|
|
328
|
+
truncated,
|
|
329
|
+
loaded: loaded.map((item) => item.filepath),
|
|
330
|
+
},
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
description: DESCRIPTION,
|
|
336
|
+
parameters: Parameters,
|
|
337
|
+
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
|
|
338
|
+
run(params, ctx).pipe(Effect.orDie),
|
|
339
|
+
}
|
|
340
|
+
}),
|
|
341
|
+
)
|
|
342
|
+
// routed through TextStream.withFallback so non-UTF-8 files are decoded via
|
|
343
|
+
// iconv. The body otherwise matches upstream.
|
|
344
|
+
export async function lines(filepath: string, opts: { limit: number; offset: number }) {
|
|
345
|
+
return TextStream.withFallback(filepath, (stream) => readLines(stream, opts))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function readLines(stream: Readable, opts: { limit: number; offset: number }) {
|
|
349
|
+
const rl = createInterface({
|
|
350
|
+
input: stream,
|
|
351
|
+
// Note: we use the crlfDelay option to recognize all instances of CR LF
|
|
352
|
+
// ('\r\n') in file as a single line break.
|
|
353
|
+
crlfDelay: Infinity,
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
const start = opts.offset - 1
|
|
357
|
+
const raw: string[] = []
|
|
358
|
+
let bytes = 0
|
|
359
|
+
let count = 0
|
|
360
|
+
let cut = false
|
|
361
|
+
let more = false
|
|
362
|
+
try {
|
|
363
|
+
for await (const text of rl) {
|
|
364
|
+
count += 1
|
|
365
|
+
if (count <= start) continue
|
|
366
|
+
|
|
367
|
+
if (raw.length >= opts.limit) {
|
|
368
|
+
more = true
|
|
369
|
+
continue
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const line = text.length > MAX_LINE_LENGTH ? text.substring(0, MAX_LINE_LENGTH) + MAX_LINE_SUFFIX : text
|
|
373
|
+
const size = Buffer.byteLength(line, "utf-8") + (raw.length > 0 ? 1 : 0)
|
|
374
|
+
if (bytes + size > MAX_BYTES) {
|
|
375
|
+
cut = true
|
|
376
|
+
more = true
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
raw.push(line)
|
|
381
|
+
bytes += size
|
|
382
|
+
}
|
|
383
|
+
} finally {
|
|
384
|
+
rl.close()
|
|
385
|
+
stream.destroy()
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { raw, count, cut, more, offset: opts.offset }
|
|
389
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Read a file or directory from the local filesystem. If the path does not exist, an error is returned.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- The filePath parameter should be an absolute path.
|
|
5
|
+
- By default, this tool returns up to 2000 lines from the start of the file.
|
|
6
|
+
- The offset parameter is the line number to start from (1-indexed).
|
|
7
|
+
- To read later sections, call this tool again with a larger offset.
|
|
8
|
+
- Use the grep tool to find specific content in large files or files with long lines.
|
|
9
|
+
- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.
|
|
10
|
+
- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\n", you will receive "1: foo\n". For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories.
|
|
11
|
+
- Any line longer than 2000 characters is truncated.
|
|
12
|
+
- Call this tool in parallel when you know there are multiple files you want to read.
|
|
13
|
+
- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.
|
|
14
|
+
- This tool can read image files and PDFs and return them as file attachments.
|