@deepseek-ai/dsh-tool-str-replace-editor 0.0.1-rc.1

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/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, DeepSeek
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/fs/tool-str-replace-editor/README.md
5
+ README.md: 8b4772cc4eb40e23a5d6ea8e409188b5033318ba
6
+ README.zh.md: b9ccf285870f2d02a2195b70dcd24fd37794976e
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @deepseek-ai/dsh-tool-str-replace-editor
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Standalone model-facing `str_replace_editor` over `ctx.fs`. It can be composed with persistent Bash, one-shot Bash, sandboxed Bash, or another terminal surface.
6
+
7
+ ## Config
8
+
9
+ | Key | Default | Meaning |
10
+ |---|---:|---|
11
+ | `maxOutputChars` | `16000` | Prefix characters retained for file and directory views. |
12
+ | `description` | Editor command guide | Model-facing tool description. |
13
+
14
+ ## Tool
15
+
16
+ The schema provides `view`, `create`, `str_replace`, and `insert` over absolute paths. File views use one-based line numbers and preserve content tabs, so displayed text remains valid literal replacement input; directory views omit hidden, dependency, and Python-cache entries and descend two levels. A metadata miss from `view`, `str_replace`, or `insert` records confirmed absence before returning `FS_NOT_FOUND`, so a later `create` can recover an externally deleted path through the mounted policy's guarded-create flow; absence never authorizes `str_replace` or `insert`. Replacement requires one unique literal match and reports errors only in the public `old_str` vocabulary. Insert follows the selected zero-based insertion boundary without adding an implicit trailing newline. Mutations preserve tabs outside the requested edit.
17
+
18
+ ## Model Experience
19
+
20
+ ### Tool schema
21
+
22
+ #### What the model sees
23
+
24
+ The generated [`str_replace_editor` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor), including the configured `description`. The plugin contributes no standalone system-prompt section.
25
+
26
+ #### Token effect
27
+
28
+ Fixed schema cost while `str_replace_editor` is visible.
29
+
30
+ #### KV Cache effect
31
+
32
+ Prefix-stable while the configured description and schema remain unchanged.
33
+
34
+ ### Tool results
35
+
36
+ #### What the model sees
37
+
38
+ Views return numbered text or a shallow directory listing. Calls expose file locations, and create/replace calls expose diff cards to presentation surfaces. Mutations return concise confirmations. Long views keep their prefix and append a clipping notice.
39
+
40
+ #### Token effect
41
+
42
+ Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice.
43
+
44
+ #### KV Cache effect
45
+
46
+ Append-only tool results follow the reusable request prefix.
47
+
48
+ ## Known Limitations and Deferred Work
49
+
50
+ - Operations target UTF-8 text; binary files are unsupported.
51
+ - `str_replace` intentionally rejects zero or multiple matches and has no `replace_all` argument.
52
+ - Every mutation goes through `fs/write-intent` or `fs/edit-intent`, resolves the current session sandbox policy, and delegates enforcement to the mounted filesystem and policy plugins.
package/README.zh.md ADDED
@@ -0,0 +1,52 @@
1
+ # @deepseek-ai/dsh-tool-str-replace-editor
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 基于 `ctx.fs` 的独立模型可见 `str_replace_editor`。它可与持久 Bash、一次性 Bash、沙箱 Bash 或其他终端接口组合。
6
+
7
+ ## 配置
8
+
9
+ | 键 | 默认值 | 含义 |
10
+ |---|---:|---|
11
+ | `maxOutputChars` | `16000` | 文件和目录查看结果保留的前缀字符数。 |
12
+ | `description` | 编辑器命令指南 | 面向模型的工具描述。 |
13
+
14
+ ## 工具
15
+
16
+ schema 提供针对绝对路径的 `view`、`create`、`str_replace` 与 `insert`。文件查看使用从 1 开始的行号,并保留内容中的制表符,因此显示的文本仍可作为有效的字面量替换输入;目录查看忽略隐藏、依赖与 Python 缓存条目并下探两层。`view`、`str_replace` 或 `insert` 发生元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前记录确认缺失,因此后续 `create` 可以通过已挂载策略的防护创建流程恢复外部删除的路径;缺失状态绝不会授权 `str_replace` 或 `insert`。替换要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇。插入遵循所选的零基插入边界,不会隐式补尾换行。修改操作会保留请求编辑范围之外的制表符。
17
+
18
+ ## 模型体验
19
+
20
+ ### 工具 schema
21
+
22
+ #### 模型看到的内容
23
+
24
+ 生成的 [`str_replace_editor` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-str-replace-editor),其中包含配置的 `description`。本插件不贡献独立系统提示词段。
25
+
26
+ #### Token 影响
27
+
28
+ `str_replace_editor` 可见时产生固定的 schema 成本。
29
+
30
+ #### KV Cache 影响
31
+
32
+ 配置的描述与 schema 不变时前缀稳定。
33
+
34
+ ### 工具结果
35
+
36
+ #### 模型看到的内容
37
+
38
+ 查看操作返回带行号文本或浅层目录列表。调用会向展示层提供文件位置,创建/替换还会提供 diff 卡片。修改操作返回简洁确认。长查看结果保留前缀并追加截断提示。
39
+
40
+ #### Token 影响
41
+
42
+ 随数据变化,并受 `maxOutputChars` 与固定截断提示约束。
43
+
44
+ #### KV Cache 影响
45
+
46
+ 工具结果以追加方式位于可复用请求前缀之后。
47
+
48
+ ## 已知限制与延后工作
49
+
50
+ - 操作面向 UTF-8 文本,不支持二进制文件。
51
+ - `str_replace` 刻意拒绝零匹配或多匹配,且没有 `replace_all` 参数。
52
+ - 每个修改操作都会经过 `fs/write-intent` 或 `fs/edit-intent`,解析当前会话的沙箱策略,并交由挂载的文件系统与策略插件执行。
package/lib/index.js ADDED
@@ -0,0 +1,341 @@
1
+ import { isAbsolute } from "node:path";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { FsError } from "@deepseek-ai/dsh-fs";
4
+ import { sandboxDenialMarker } from "@deepseek-ai/dsh-sandbox";
5
+ import { defineTool } from "@deepseek-ai/dsh-tools";
6
+ //#region lib/types/index.js
7
+ /**
8
+ * Model-facing `str_replace_editor` over the Harness filesystem seam.
9
+ * @module @deepseek-ai/dsh-tool-str-replace-editor
10
+ */
11
+ const TRUNCATED_MESSAGE = "<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>";
12
+ const DEFAULT_DESCRIPTION = `
13
+ Custom editing tool for viewing, creating and editing files
14
+ * State is persistent across command calls and discussions with the user
15
+ * If \`path\` is a file, \`view\` displays the result of applying \`cat -n\`. If \`path\` is a directory, \`view\` lists non-hidden files and directories up to 2 levels deep
16
+ * The \`create\` command cannot be used if the specified \`path\` already exists as a file
17
+ * If a \`command\` generates a long output, it will be truncated and marked with \`<response clipped>\`
18
+
19
+ Notes for using the \`str_replace\` command:
20
+ * The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
21
+ * If the \`old_str\` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in \`old_str\` to make it unique
22
+ * The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\`
23
+ `.trim();
24
+ function maybeTruncate(content, maxOutputChars) {
25
+ return content.length <= maxOutputChars ? content : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE;
26
+ }
27
+ function codepointCompare(left, right) {
28
+ return left < right ? -1 : left > right ? 1 : 0;
29
+ }
30
+ function matchOffsets(content, search) {
31
+ const offsets = [];
32
+ let offset = 0;
33
+ while (true) {
34
+ const match = content.indexOf(search, offset);
35
+ if (match < 0) return offsets;
36
+ offsets.push(match);
37
+ offset = match + search.length;
38
+ }
39
+ }
40
+ function lineNumbersAt(content, offsets) {
41
+ let line = 1;
42
+ let cursor = 0;
43
+ return offsets.map((offset) => {
44
+ while (cursor < offset) {
45
+ if (content[cursor] === "\n") line += 1;
46
+ cursor += 1;
47
+ }
48
+ return line;
49
+ });
50
+ }
51
+ var MutationPolicy = class {
52
+ policy;
53
+ constructor(ctx) {
54
+ this.policy = ctx.fs.sandboxMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
55
+ if (ctx.fs.sandboxMode !== void 0 && this.policy === void 0) throw new Error("tool-str-replace-editor: the mounted filesystem confines but ctx.sandboxPolicy is missing");
56
+ }
57
+ resolve(exec) {
58
+ return this.policy?.resolve({ ...exec.agent === void 0 ? {} : { session: exec.agent.session } });
59
+ }
60
+ mapError(error, policy) {
61
+ if (!(error instanceof FsError) || error.code !== "FS_SANDBOX_DENIED") return error;
62
+ const mode = policy.mode;
63
+ return new FsError(sandboxDenialMarker(mode), "FS_SANDBOX_DENIED", { cause: error });
64
+ }
65
+ };
66
+ async function resolveTarget(ctx, path, signal) {
67
+ if (path.trim().length === 0) throw new Error("path must be a non-empty string");
68
+ if (!isAbsolute(path)) throw new Error(`The path ${path} is not an absolute path, it should start with \`/\`. Maybe you meant /${path}?`);
69
+ return ctx.fs.resolve(path, { signal });
70
+ }
71
+ async function statExisting(ctx, target, command, exec) {
72
+ const info = await ctx.fs.stat(target, exec.signal);
73
+ if (info === void 0) {
74
+ ctx.emit("fs/observed", target, { kind: "absent" }, exec);
75
+ throw new FsError(`The path ${target.displayPath} does not exist. Please provide a valid path.`, "FS_NOT_FOUND");
76
+ }
77
+ if (info.type === "directory" && command !== "view") throw new FsError(`The path ${target.displayPath} is a directory and only the \`view\` command can be used on directories`, "FS_NOT_REGULAR_FILE");
78
+ return info;
79
+ }
80
+ function requiredForCommand(value, parameter, command, allowEmpty = true) {
81
+ if (value === void 0) throw new Error(`Parameter \`${parameter}\` is required for command: ${command}`);
82
+ if (!allowEmpty && value.length === 0) throw new Error(`Parameter \`${parameter}\` is empty for command: ${command}`);
83
+ return value;
84
+ }
85
+ function formatFileView(path, content, maxOutputChars, viewRange) {
86
+ const allLines = content.split("\n");
87
+ let lines = allLines;
88
+ let initialLine = 1;
89
+ let finalLine;
90
+ let prompt = `Here's the content of ${path} with line numbers (which has a total of ${allLines.length} lines)`;
91
+ if (viewRange !== void 0) {
92
+ const [requestedInitialLine, requestedFinalLine] = viewRange;
93
+ if (viewRange.length !== 2 || requestedInitialLine === void 0 || requestedFinalLine === void 0 || !viewRange.every(Number.isInteger)) throw new Error("Invalid `view_range`. It should be a list of two integers.");
94
+ initialLine = requestedInitialLine;
95
+ finalLine = requestedFinalLine;
96
+ if (initialLine < 1 || initialLine > allLines.length) throw new Error(`Invalid \`view_range\`: [${viewRange.join(", ")}]. Its first element \`${initialLine}\` should be within the range of lines of the file: [1, ${allLines.length}]`);
97
+ if (finalLine > allLines.length) throw new Error(`Invalid \`view_range\`: [${viewRange.join(", ")}]. Its second element \`${finalLine}\` should be smaller than the number of lines in the file: \`${allLines.length}\``);
98
+ if (finalLine !== -1 && finalLine < initialLine) throw new Error(`Invalid \`view_range\`: [${viewRange.join(", ")}]. Its second element \`${finalLine}\` should be larger or equal than its first \`${initialLine}\``);
99
+ lines = finalLine === -1 ? allLines.slice(initialLine - 1) : allLines.slice(initialLine - 1, finalLine);
100
+ prompt += ` with view_range=[${initialLine}, ${finalLine}]`;
101
+ }
102
+ const numbered = lines.map((line, index) => `${String(initialLine + index).padStart(6, " ")} ${line}`).join("\n");
103
+ return maybeTruncate(`${prompt}:\n${numbered}\n`, maxOutputChars);
104
+ }
105
+ async function listDirectory(ctx, target, maxOutputChars, exec) {
106
+ async function visit(dir, depth) {
107
+ const entries = await ctx.fs.listDir(dir, exec.signal);
108
+ const rows = [];
109
+ for (const entry of entries.filter((candidate) => !candidate.name.startsWith(".") && candidate.name !== "node_modules" && candidate.name !== "__pycache__")) {
110
+ const type = entry.type === "directory" ? "d" : entry.type === "file" ? "f" : "?";
111
+ rows.push(`${type}\t${entry.target.displayPath}`);
112
+ if (entry.type === "directory" && depth < 2) rows.push(...await visit(entry.target, depth + 1));
113
+ }
114
+ return rows;
115
+ }
116
+ const rows = [`d\t${target.displayPath}`, ...await visit(target, 1)];
117
+ rows.sort((left, right) => {
118
+ return codepointCompare(left.slice(left.indexOf(" ") + 1), right.slice(right.indexOf(" ") + 1));
119
+ });
120
+ const listing = maybeTruncate(rows.join("\n") + "\n", maxOutputChars);
121
+ return `Here're the files and directories up to 2 levels deep in ${target.displayPath}, excluding hidden items, node_modules, and Python cache directories:\n${listing}\n`;
122
+ }
123
+ async function viewPath(ctx, path, viewRange, maxOutputChars, exec) {
124
+ const target = await resolveTarget(ctx, path, exec.signal);
125
+ const info = await statExisting(ctx, target, "view", exec);
126
+ if (info.type === "directory") {
127
+ if (viewRange !== void 0) throw new Error("The `view_range` parameter is not allowed when `path` points to a directory.");
128
+ return listDirectory(ctx, target, maxOutputChars, exec);
129
+ }
130
+ if (info.type !== "file") throw new FsError(`cannot view "${target.displayPath}": not a regular file or directory`, "FS_NOT_REGULAR_FILE");
131
+ const content = await ctx.fs.readText(target, exec.signal);
132
+ ctx.emit("fs/observed", target, {
133
+ kind: "present",
134
+ version: info.version
135
+ }, exec);
136
+ return formatFileView(target.displayPath, content, maxOutputChars, viewRange);
137
+ }
138
+ async function createFile(ctx, policy, path, fileText, exec) {
139
+ const content = requiredForCommand(fileText, "file_text", "create");
140
+ const sandboxPolicy = policy.resolve(exec);
141
+ const target = await resolveTarget(ctx, path, exec.signal);
142
+ if (await ctx.fs.stat(target, exec.signal) !== void 0) throw new Error(`File already exists at: ${target.displayPath}. Cannot overwrite files using command \`create\`.`);
143
+ const intent = await ctx.waterfall("fs/write-intent", target, exec, () => ({ kind: "createIfAbsent" }));
144
+ let outcome;
145
+ try {
146
+ outcome = await ctx.fs.writeText(target, content, intent, exec.signal, sandboxPolicy);
147
+ } catch (error) {
148
+ throw policy.mapError(error, sandboxPolicy);
149
+ }
150
+ ctx.emit("fs/observed", target, {
151
+ kind: "present",
152
+ version: outcome.version
153
+ }, exec);
154
+ return `New file created successfully at: ${target.displayPath}`;
155
+ }
156
+ async function replaceInFile(ctx, policy, path, oldStr, newStr, exec) {
157
+ const sandboxPolicy = policy.resolve(exec);
158
+ const target = await resolveTarget(ctx, path, exec.signal);
159
+ const intent = await ctx.waterfall("fs/edit-intent", target, exec, () => void 0);
160
+ const oldValue = requiredForCommand(oldStr, "old_str", "str_replace", false);
161
+ const newValue = newStr ?? "";
162
+ const info = await statExisting(ctx, target, "str_replace", exec);
163
+ if (info.type !== "file") throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
164
+ const before = await ctx.fs.readText(target, exec.signal);
165
+ const offsets = matchOffsets(before, oldValue);
166
+ const offset = offsets[0];
167
+ if (offset === void 0) throw new FsError(`No replacement was performed, old_str \`${oldValue}\` did not appear verbatim in ${target.displayPath}.`, "FS_EDIT_NOT_FOUND");
168
+ if (offsets.length > 1) throw new FsError(`No replacement was performed. Multiple occurrences of old_str \`${oldValue}\` in lines [${lineNumbersAt(before, offsets).join(", ")}]. Please ensure it is unique`, "FS_AMBIGUOUS_EDIT");
169
+ let outcome;
170
+ try {
171
+ outcome = await ctx.fs.writeText(target, before.slice(0, offset) + newValue + before.slice(offset + oldValue.length), intent === void 0 ? {
172
+ kind: "replaceIfVersion",
173
+ version: info.version
174
+ } : {
175
+ kind: "replaceIfVersion",
176
+ version: intent.version
177
+ }, exec.signal, sandboxPolicy);
178
+ } catch (error) {
179
+ throw policy.mapError(error, sandboxPolicy);
180
+ }
181
+ ctx.emit("fs/observed", target, {
182
+ kind: "present",
183
+ version: outcome.version
184
+ }, exec);
185
+ return `The file ${target.displayPath} has been edited successfully.`;
186
+ }
187
+ async function insertInFile(ctx, policy, path, insertLine, newStr, exec) {
188
+ if (insertLine === void 0) throw new Error("Parameter `insert_line` is required for command: insert");
189
+ const value = requiredForCommand(newStr, "new_str", "insert");
190
+ const sandboxPolicy = policy.resolve(exec);
191
+ const target = await resolveTarget(ctx, path, exec.signal);
192
+ const intent = await ctx.waterfall("fs/edit-intent", target, exec, () => void 0);
193
+ const info = await statExisting(ctx, target, "insert", exec);
194
+ if (info.type !== "file") throw new FsError(`cannot insert into "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
195
+ const lines = (await ctx.fs.readText(target, exec.signal)).split("\n");
196
+ if (!Number.isInteger(insertLine) || insertLine < 0 || insertLine > lines.length) throw new Error(`Invalid \`insert_line\` parameter: ${insertLine}. It should be within the range of lines of the file: [0, ${lines.length}]`);
197
+ const after = [
198
+ ...lines.slice(0, insertLine),
199
+ ...value.split("\n"),
200
+ ...lines.slice(insertLine)
201
+ ].join("\n");
202
+ const expected = intent === void 0 ? {
203
+ kind: "replaceIfVersion",
204
+ version: info.version
205
+ } : {
206
+ kind: "replaceIfVersion",
207
+ version: intent.version
208
+ };
209
+ let outcome;
210
+ try {
211
+ outcome = await ctx.fs.writeText(target, after, expected, exec.signal, sandboxPolicy);
212
+ } catch (error) {
213
+ throw policy.mapError(error, sandboxPolicy);
214
+ }
215
+ ctx.emit("fs/observed", target, {
216
+ kind: "present",
217
+ version: outcome.version
218
+ }, exec);
219
+ return `The file ${target.displayPath} has been edited successfully.`;
220
+ }
221
+ function presentEditorCall(args) {
222
+ switch (args.command) {
223
+ case "view": return {
224
+ card: "generic",
225
+ title: `view ${args.path}`,
226
+ kind: "read",
227
+ locations: [{ path: args.path }]
228
+ };
229
+ case "create": return {
230
+ card: "diff",
231
+ title: `create ${args.path}`,
232
+ diffs: [{
233
+ path: args.path,
234
+ oldText: null,
235
+ newText: args.file_text ?? ""
236
+ }],
237
+ locations: [{ path: args.path }]
238
+ };
239
+ case "str_replace": return {
240
+ card: "diff",
241
+ title: `str_replace ${args.path}`,
242
+ diffs: [{
243
+ path: args.path,
244
+ oldText: args.old_str ?? null,
245
+ newText: args.new_str ?? ""
246
+ }],
247
+ locations: [{ path: args.path }]
248
+ };
249
+ case "insert": return {
250
+ card: "generic",
251
+ title: `insert ${args.path}`,
252
+ kind: "edit",
253
+ locations: [{
254
+ path: args.path,
255
+ ...args.insert_line === void 0 ? {} : { line: Math.max(1, args.insert_line + 1) }
256
+ }]
257
+ };
258
+ }
259
+ }
260
+ /** Register the model-facing `str_replace_editor` tool. */
261
+ function registerStrReplaceEditor(ctx, config) {
262
+ const policy = new MutationPolicy(ctx);
263
+ ctx.tools.register(defineTool({
264
+ name: "str_replace_editor",
265
+ description: config.description,
266
+ parameters: {
267
+ command: {
268
+ type: "string",
269
+ required: true,
270
+ enum: [
271
+ "view",
272
+ "create",
273
+ "str_replace",
274
+ "insert"
275
+ ],
276
+ description: "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`."
277
+ },
278
+ path: {
279
+ type: "string",
280
+ required: true,
281
+ description: "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
282
+ },
283
+ file_text: {
284
+ type: "string",
285
+ description: "Required parameter of `create` command, with the content of the file to be created."
286
+ },
287
+ insert_line: {
288
+ type: "integer",
289
+ description: "Required parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`."
290
+ },
291
+ new_str: {
292
+ type: "string",
293
+ description: "Optional parameter of `str_replace` command containing the new string (if not given, no string will be added). Required parameter of `insert` command containing the string to insert."
294
+ },
295
+ old_str: {
296
+ type: "string",
297
+ description: "Required parameter of `str_replace` command containing the string in `path` to replace."
298
+ },
299
+ view_range: {
300
+ type: "array",
301
+ items: { type: "integer" },
302
+ description: "Optional parameter of `view` command when `path` points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting `[start_line, -1]` shows all lines from `start_line` to the end of the file."
303
+ }
304
+ },
305
+ output: {
306
+ schema: { type: "string" },
307
+ render: (_args, value) => [{
308
+ type: "text",
309
+ text: value
310
+ }]
311
+ },
312
+ async execute(args, exec) {
313
+ switch (args.command) {
314
+ case "view": return viewPath(ctx, args.path, args.view_range, config.maxOutputChars, exec);
315
+ case "create": return createFile(ctx, policy, args.path, args.file_text, exec);
316
+ case "str_replace": return replaceInFile(ctx, policy, args.path, args.old_str, args.new_str, exec);
317
+ case "insert": return insertInFile(ctx, policy, args.path, args.insert_line, args.new_str, exec);
318
+ }
319
+ },
320
+ presentCall: presentEditorCall
321
+ }));
322
+ }
323
+ const name = "tool-str-replace-editor";
324
+ const inject = ["tools", "fs"];
325
+ /** Runtime configuration schema for the string-replacement editor tool. */
326
+ const Config = z.object({
327
+ maxOutputChars: z.number().default(16e3),
328
+ description: z.string().default(DEFAULT_DESCRIPTION)
329
+ });
330
+ /** Register one `str_replace_editor` tool over `ctx.fs`. */
331
+ function apply(ctx, config) {
332
+ const resolved = {
333
+ maxOutputChars: config.maxOutputChars ?? 16e3,
334
+ description: config.description ?? DEFAULT_DESCRIPTION
335
+ };
336
+ if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) throw new Error("tool-str-replace-editor: maxOutputChars must be a positive safe integer");
337
+ if (resolved.description.trim().length === 0) throw new Error("tool-str-replace-editor: description must be non-empty");
338
+ registerStrReplaceEditor(ctx, resolved);
339
+ }
340
+ //#endregion
341
+ export { Config, apply, inject, name };
@@ -0,0 +1,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@deepseek-ai/dsh-tool-str-replace-editor`.
4
+ * @module @deepseek-ai/dsh-tool-str-replace-editor/invariant
5
+ */
6
+ const PACKAGE_NAME = "@deepseek-ai/dsh-tool-str-replace-editor";
7
+ /** Cordis companion plugin name. */
8
+ const name = "tool-str-replace-editor-invariant";
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ["invariants"];
11
+ /**
12
+ * No runtime invariant: the tool adapter owns no independent durable state;
13
+ * filesystem mutation relations stay with the provider and policy plugins.
14
+ */
15
+ const install = () => {};
16
+ /**
17
+ * Register this package's invariant companion.
18
+ * @param ctx - Cordis context carrying the invariant service.
19
+ * @returns the installed registration's disposer after setup succeeds.
20
+ */
21
+ const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
+ //#endregion
23
+ export { apply, inject, name };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Model-facing `str_replace_editor` over the Harness filesystem seam.
3
+ * @module @deepseek-ai/dsh-tool-str-replace-editor
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ import z from '@deepseek-ai/schemastery';
7
+ export declare const name = "tool-str-replace-editor";
8
+ export declare const inject: string[];
9
+ /** Configuration for the string-replacement editor tool. */
10
+ export interface Config {
11
+ /** Maximum returned view characters before clipping (default 16000). */
12
+ maxOutputChars?: number;
13
+ /** Model-facing tool description. */
14
+ description?: string;
15
+ }
16
+ /** Runtime configuration schema for the string-replacement editor tool. */
17
+ export declare const Config: z<Config>;
18
+ /** Register one `str_replace_editor` tool over `ctx.fs`. */
19
+ export declare function apply(ctx: Context, config: Config): void;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@deepseek-ai/dsh-tool-str-replace-editor`.
3
+ * @module @deepseek-ai/dsh-tool-str-replace-editor/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "tool-str-replace-editor-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Cordis context carrying the invariant service.
13
+ * @returns the installed registration's disposer after setup succeeds.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@deepseek-ai/dsh-tool-str-replace-editor",
3
+ "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service",
4
+ "version": "0.0.1-rc.1",
5
+ "publishConfig": {
6
+ "access": "restricted"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/fs/tool-str-replace-editor"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./invariant": {
22
+ "types": "./lib/types/invariant.d.ts",
23
+ "default": "./lib/invariant.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/invariant.js",
30
+ "lib/types/**/*.d.ts"
31
+ ],
32
+ "license": "BSD-3-Clause",
33
+ "peerDependencies": {
34
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
35
+ "@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
36
+ "@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.1",
37
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
38
+ "@deepseek-ai/dsh-fs": "^0.0.1-rc.1",
39
+ "@deepseek-ai/cordis": "^4.0.1-rc.1"
40
+ },
41
+ "dependencies": {
42
+ "@deepseek-ai/schemastery": "^3.18.1-rc.1"
43
+ },
44
+ "devDependencies": {
45
+ "@deepseek-ai/dsh-agent": "^0.0.1-rc.1",
46
+ "@deepseek-ai/dsh-fs": "^0.0.1-rc.1",
47
+ "@deepseek-ai/dsh-fs-local": "^0.0.1-rc.1",
48
+ "@deepseek-ai/dsh-fs-policy": "^0.0.1-rc.1",
49
+ "@deepseek-ai/dsh-fs-sandbox": "^0.0.1-rc.1",
50
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
51
+ "@deepseek-ai/dsh-sandbox-policy": "^0.0.1-rc.1",
52
+ "@deepseek-ai/dsh-sandbox": "^0.0.1-rc.1",
53
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
54
+ "@deepseek-ai/cordis": "^4.0.1-rc.1",
55
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
56
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
57
+ "@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1"
58
+ }
59
+ }