@qilin-agent/tool-str-replace-editor 0.0.0-stage → 3.1.3

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kkutysllb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,79 @@
1
+ # Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
2
+ # section, a hash of its English and Chinese blocks outside code blocks and generated regions.
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
+ /:
6
+ en: 1d308889b00602e0
7
+ zh: 8858e4768891e79d
8
+ /qilin-tool-str-replace-editor:
9
+ en: 75986c32c840444f
10
+ zh: 5009d9b5846752b0
11
+ /qilin-tool-str-replace-editor/summary:
12
+ en: 6459bca2750b3ded
13
+ zh: 7e88be2960a372f4
14
+ /qilin-tool-str-replace-editor/table-of-contents:
15
+ en: d152484eb41ac6b4
16
+ zh: 09388d293f9be9cb
17
+ /qilin-tool-str-replace-editor/use-this-package:
18
+ en: 00b16ec00064922b
19
+ zh: 5e0f3e78f1bb4a29
20
+ /qilin-tool-str-replace-editor/use-this-package/minimal-composition:
21
+ en: db6e7d91ff7e0439
22
+ zh: 2ed4096f6c856e17
23
+ /qilin-tool-str-replace-editor/use-this-package/configuration:
24
+ en: e6860f1861cc0df8
25
+ zh: cd00d3c1a660b7c7
26
+ /qilin-tool-str-replace-editor/use-this-package/the-commands:
27
+ en: 554e8446946fbd96
28
+ zh: 3af12627c19a0111
29
+ /qilin-tool-str-replace-editor/use-this-package/failures-and-recovery:
30
+ en: 2b249fcb0b5621aa
31
+ zh: d172b3de43c337c4
32
+ /qilin-tool-str-replace-editor/understand-the-implementation:
33
+ en: b0b36a22cd4421eb
34
+ zh: db47ef5b71dd51df
35
+ /qilin-tool-str-replace-editor/understand-the-implementation/design-concept:
36
+ en: cce2bf460652cba4
37
+ zh: 1f4f1c1563c4c55c
38
+ /qilin-tool-str-replace-editor/understand-the-implementation/source-map:
39
+ en: 3f906cb8a23140e5
40
+ zh: 5d72b0f3244c29bc
41
+ /qilin-tool-str-replace-editor/understand-the-implementation/how-each-command-runs:
42
+ en: a866aa948afff207
43
+ zh: 65b0417597efc19c
44
+ /qilin-tool-str-replace-editor/further-exploration:
45
+ en: 577f4fe9e2b2bcae
46
+ zh: 6e7fc23f7efd3ad7
47
+ /qilin-tool-str-replace-editor/model-experience:
48
+ en: 215e7ba838619b7b
49
+ zh: a311da3843709f90
50
+ /qilin-tool-str-replace-editor/model-experience/tool-schema:
51
+ en: e4c7376d4f29dbd5
52
+ zh: 6b7a9eaf69c9ea53
53
+ /qilin-tool-str-replace-editor/model-experience/tool-schema/what-the-model-sees:
54
+ en: 9dd033176c0d4e22
55
+ zh: 894648999fc95757
56
+ /qilin-tool-str-replace-editor/model-experience/tool-schema/token-effect:
57
+ en: b4009b2b48c27168
58
+ zh: f8692c0c87c7ce69
59
+ /qilin-tool-str-replace-editor/model-experience/tool-schema/kv-cache-effect:
60
+ en: 7d9323583a83cb95
61
+ zh: 726ba31a296c1b5e
62
+ /qilin-tool-str-replace-editor/model-experience/tool-results:
63
+ en: beaabb79f229692a
64
+ zh: 8573891441963be4
65
+ /qilin-tool-str-replace-editor/model-experience/tool-results/what-the-model-sees:
66
+ en: 2ce8dee1a9711311
67
+ zh: aed7d8e758710c1e
68
+ /qilin-tool-str-replace-editor/model-experience/tool-results/token-effect:
69
+ en: b60b4bccff75a5e3
70
+ zh: 100e97b019030507
71
+ /qilin-tool-str-replace-editor/model-experience/tool-results/kv-cache-effect:
72
+ en: ba6a48888c3361e5
73
+ zh: 30244b70b2122bd6
74
+ /qilin-tool-str-replace-editor/known-limitations-and-deferred-work:
75
+ en: bce77fc35b8a0140
76
+ zh: 48f227222bf5f3dc
77
+ /qilin-tool-str-replace-editor/known-limitations-and-deferred-work/dev-note:
78
+ en: e67f633dc6fe7773
79
+ zh: 3bb0c20c93f16f98
package/README.md CHANGED
@@ -1,3 +1,147 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "The standalone str_replace_editor tool over ctx.fs for users and maintainers composing Claude-Code-style file editing for agents."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # @qilin-agent/tool-str-replace-editor
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `qilin-tool-str-replace-editor` provides a standalone model-facing `str_replace_editor` tool over `ctx.fs`: `view` shows numbered file content or a shallow directory listing, `create` makes a new file, `str_replace` applies a unique literal replacement, and `insert` adds lines at a chosen boundary. It is composable with persistent Bash, one-shot Bash, sandboxed Bash, or another terminal surface. Mutations obey the same read-before-edit policy and sandbox fence as the rest of the fs family, enforced by whichever backend and policy plugins are mounted. Choose it when a deployment wants the Claude-Code-style single editor tool with absolute paths; the `qilin-tool-fs` package provides the alternative `read`/`write`/`edit` suite.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Mount the tool alongside a `ctx.fs` backend (and, for guarded mutations, the policy plugin) when the model should edit files through the familiar `view`/`create`/`str_replace`/`insert` command vocabulary on absolute paths.
29
+
30
+ ### Minimal composition
31
+
32
+ A backend, optionally the policy plugin, then the tool; the editor composes with any terminal surface.
33
+
34
+ ```yaml
35
+ - name: '@qilin-agent/fs-local'
36
+ - name: '@qilin-agent/fs-observation-policy'
37
+ - name: '@qilin-agent/tool-str-replace-editor'
38
+ ```
39
+
40
+ ### Configuration
41
+
42
+ | Key | Default | Meaning |
43
+ |---|---|---|
44
+ | `maxOutputChars` | `16000` | Prefix characters retained for file and directory views |
45
+ | `description` | `Custom editing tool for viewing, creating and editing files` (multi-line) | Model-facing tool description |
46
+
47
+ ### The commands
48
+
49
+ `view` returns one-based numbered file content (tabs preserved, so displayed text stays valid literal replacement input) or a two-level directory listing that omits hidden, dependency, and Python-cache entries. `create` makes a new file and refuses to overwrite an existing one. Command-specific fields may contain `null` placeholders when the selected command does not use them; required fields stay required, `view_range: null` selects the full view, and `str_replace.new_str: null` is rejected so deletion requires omission. `str_replace` requires one unique literal match, with errors reported 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.
50
+
51
+ ### Failures and recovery
52
+
53
+ 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`. Guarded mutations inherit the policy plugin's codes and remedies — `FS_NOT_OBSERVED` (read the file, then retry), `FS_STALE_VERSION` (re-read, then retry) — and sandbox denials surface as the `[sandbox: file access denied under <mode> mode]` marker. Paths must be absolute; a relative path is refused with a hint.
54
+
55
+ -----
56
+
57
+ <a id="understand-the-implementation"></a>
58
+ ## Understand the implementation
59
+
60
+ <details>
61
+ <summary>Implementation internals — click to expand</summary>
62
+
63
+ This section explains the design decisions behind the editor tool and points at the code that realizes them; the observable behavior is fully covered in [Use this package](#use-this-package).
64
+
65
+ ### Design concept
66
+
67
+ The tool is one schema with four commands over `ctx.fs`. Mutations never touch the provider directly with their own assumptions: each one runs the `fs/write-intent` or `fs/edit-intent` waterfall to obtain the policy plugin's guard, resolves the per-call sandbox policy when the mounted `ctx.fs` confines, and delegates enforcement to the provider. `str_replace` and `insert` additionally re-read the file and use the observed version as the compare-and-swap basis when no policy plugin supplies a guard.
68
+
69
+ ### Source map
70
+
71
+ | File | Role |
72
+ |---|---|
73
+ | [`src/index.ts`](src/index.ts) | The whole tool: schema, command dispatch, view rendering, mutation policy |
74
+
75
+ ### How each command runs
76
+
77
+ Every command resolves the absolute path first; mutations then follow one shared flow — policy guard, provider enforcement, then an `fs/observed` record on success — while `view` only stats and renders. The entire tool — schema, command dispatch, and view rendering — lives in `src/index.ts`.
78
+
79
+ </details>
80
+
81
+ -----
82
+
83
+ <a id="further-exploration"></a>
84
+ ## Further Exploration
85
+
86
+ Read these pages when the package-level contract is not enough. They move from the tool to the contract, policy, and backends it composes with.
87
+
88
+ - [Filesystem subsystem](../../../docs/subsystems/filesystem.md) — exhaustive provider contract, policy events, and error taxonomy.
89
+ - [qilin-fs](../fs/README.md) — the `ctx.fs` contract this tool consumes.
90
+ - [tool-fs](../tool-fs/README.md) — the alternative `read`/`write`/`edit` tool suite.
91
+ - [fs-observation-policy](../fs-observation-policy/README.md) — the policy plugin that guards mutations through the `fs/*` events.
92
+ - [fs-sandbox](../fs-sandbox/README.md) — the sandbox-enforcing backend that fences mutations.
93
+ - [Generated tool catalog](../../../docs/tool-catalog.md#qilin-agenttool-str-replace-editor) — the exhaustive schema this package registers.
94
+
95
+ -----
96
+
97
+ <a id="model-experience"></a>
98
+ ## Model Experience
99
+
100
+ ### Tool schema
101
+
102
+ #### What the model sees
103
+
104
+ The generated [`str_replace_editor` schema](../../../docs/tool-catalog.md#qilin-agenttool-str-replace-editor), including the configured `description`. The plugin contributes no standalone system-prompt section.
105
+
106
+ #### Token effect
107
+
108
+ Fixed schema cost while `str_replace_editor` is visible.
109
+
110
+ #### KV Cache effect
111
+
112
+ Prefix-stable while the configured description and schema remain unchanged.
113
+
114
+ ### Tool results
115
+
116
+ #### What the model sees
117
+
118
+ 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.
119
+
120
+ #### Token effect
121
+
122
+ Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice.
123
+
124
+ #### KV Cache effect
125
+
126
+ Append-only tool results follow the reusable request prefix.
127
+
128
+ ## Known Limitations and Deferred Work
129
+
130
+ <a id="known-limitations-and-deferred-work"></a>
131
+
132
+
133
+ These limits define when the editor tool is a poor fit or needs special operational care. They are current package constraints, not a general editor comparison or a task backlog.
134
+
135
+ - **Operations target UTF-8 text** — binary files are unsupported.
136
+ - **`str_replace` intentionally rejects zero or multiple matches** — it has no `replace_all` argument.
137
+ - **Every mutation goes through the mounted policy and sandbox** — `fs/write-intent` or `fs/edit-intent` resolves the current session sandbox policy and delegates enforcement to the mounted filesystem and policy plugins, so a deployment without them gets unconditional mutations.
138
+
139
+ <a id="dev-note"></a>
140
+ ### Dev Note
141
+
142
+ <details>
143
+ <summary>Working context for maintainers — click to expand</summary>
144
+
145
+ None.
146
+
147
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,147 @@
1
+ ---
2
+ description: "基于 `ctx.fs` 的独立 str_replace_editor 工具:供为 agent(智能体)组合 Claude Code 风格文件编辑能力的用户与维护者使用。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @qilin-agent/tool-str-replace-editor
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `qilin-tool-str-replace-editor` 提供基于 `ctx.fs` 的独立面向模型 `str_replace_editor` 工具:`view` 显示带行号的文件内容或浅层目录列表,`create` 创建新文件,`str_replace` 应用唯一的字面量替换,`insert` 在选定的边界处插入行。它可以与持久 Bash、一次性 Bash、沙箱 Bash 或其他终端接口组合。修改操作遵守与 fs 家族其余部分相同的编辑前读取策略与沙箱围栏,具体由所挂载的后端与策略插件强制执行。当部署需要 Claude Code 风格、使用绝对路径的单一编辑器工具时选择它;`qilin-tool-fs` 包提供替代的 `read`/`write`/`edit` 套件。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 当模型应通过熟悉的 `view`/`create`/`str_replace`/`insert` 命令词汇在绝对路径上编辑文件时,把工具与 `ctx.fs` 后端(以及需要防护变更时的策略插件)一起挂载。
29
+
30
+ ### 最小组合
31
+
32
+ 一个后端、可选地加策略插件,然后是工具;编辑器可与任何终端接口组合。
33
+
34
+ ```yaml
35
+ - name: '@qilin-agent/fs-local'
36
+ - name: '@qilin-agent/fs-observation-policy'
37
+ - name: '@qilin-agent/tool-str-replace-editor'
38
+ ```
39
+
40
+ ### 配置
41
+
42
+ | 键 | 默认值 | 含义 |
43
+ |---|---|---|
44
+ | `maxOutputChars` | `16000` | 文件和目录查看结果保留的前缀字符数 |
45
+ | `description` | `Custom editing tool for viewing, creating and editing files`(多行) | 面向模型的工具描述 |
46
+
47
+ ### 命令
48
+
49
+ `view` 返回从 1 开始编号的文件内容(保留制表符,因此显示的文本仍可作为有效的字面量替换输入)或忽略隐藏、依赖与 Python 缓存条目的两层目录列表。`create` 创建新文件,并拒绝覆盖现有文件。选定命令不使用的专用字段可以包含 `null` 占位符;必填字段仍保持必填,`view_range: null` 选择完整视图,`str_replace.new_str: null` 会被拒绝,因此删除内容必须省略该字段。`str_replace` 要求字面量唯一匹配,错误只使用公开的 `old_str` 词汇;`insert` 遵循所选的零基插入边界,不会隐式补尾换行。修改操作会保留请求编辑范围之外的制表符。
50
+
51
+ ### 失败与恢复
52
+
53
+ `view`、`str_replace` 或 `insert` 发生元数据未命中时,工具会在返回 `FS_NOT_FOUND` 前记录确认缺失,因此后续 `create` 可以通过已挂载策略的防护创建流程恢复外部删除的路径;缺失状态绝不会授权 `str_replace` 或 `insert`。防护变更继承策略插件的错误码与恢复指令——`FS_NOT_OBSERVED`(先读取文件再重试)、`FS_STALE_VERSION`(先重新读取再重试)——沙箱拒绝则表现为 `[sandbox: file access denied under <mode> mode]` 标记。路径必须是绝对路径;相对路径会被拒绝并给出提示。
54
+
55
+ -----
56
+
57
+ <a id="understand-the-implementation"></a>
58
+ ## 理解实现
59
+
60
+ <details>
61
+ <summary>实现细节——点击展开</summary>
62
+
63
+ 本节解释编辑器工具背后的设计决策,并指出实现它们的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
64
+
65
+ ### 设计理念
66
+
67
+ 该工具是基于 `ctx.fs` 的单一 schema、四个命令。修改操作绝不带着自己的假设直接触碰提供方:每个操作都运行 `fs/write-intent` 或 `fs/edit-intent` waterfall(瀑布式事件)以取得策略插件的防护,在已挂载的 `ctx.fs` 实施沙箱限制时解析每次调用的沙箱策略,并把强制执行委托给提供方。`str_replace` 与 `insert` 还会重新读取文件,并在没有策略插件提供防护时把观察到的版本作为比较并交换的基础。
68
+
69
+ ### 源码地图
70
+
71
+ | 文件 | 职责 |
72
+ |---|---|
73
+ | [`src/index.ts`](src/index.ts) | 整个工具:schema、命令分派、查看渲染、修改策略 |
74
+
75
+ ### 各命令如何运行
76
+
77
+ 每个命令都先解析绝对路径;修改操作随后遵循同一条共享流程——策略防护、提供方强制执行、成功后记录 `fs/observed`——而 `view` 只执行 stat 并渲染。整个工具——schema、命令分派与查看渲染——都位于 `src/index.ts`。
78
+
79
+ </details>
80
+
81
+ -----
82
+
83
+ <a id="further-exploration"></a>
84
+ ## 进一步探索
85
+
86
+ 当包级约定不够用时阅读以下页面。它们从工具逐步进入它所组合的约定、策略与后端。
87
+
88
+ - [文件系统子系统](../../../docs/subsystems/filesystem.zh.md)——穷尽式提供方约定、策略事件与错误分类体系。
89
+ - [qilin-fs](../fs/README.zh.md)——本工具消费的 `ctx.fs` 约定。
90
+ - [tool-fs](../tool-fs/README.zh.md)——替代的 `read`/`write`/`edit` 工具套件。
91
+ - [fs-observation-policy](../fs-observation-policy/README.zh.md)——通过 `fs/*` 事件防护变更的策略插件。
92
+ - [fs-sandbox](../fs-sandbox/README.zh.md)——围栏变更的沙箱强制后端。
93
+ - [生成工具目录](../../../docs/tool-catalog.zh.md#qilin-agenttool-str-replace-editor)——本包注册的穷尽式 schema。
94
+
95
+ -----
96
+
97
+ <a id="model-experience"></a>
98
+ ## 模型体验
99
+
100
+ ### 工具 schema
101
+
102
+ #### 模型看到的内容
103
+
104
+ 生成的 [`str_replace_editor` schema](../../../docs/tool-catalog.zh.md#qilin-agenttool-str-replace-editor),包含配置的 `description`。本插件不贡献独立系统提示词段。
105
+
106
+ #### Token 影响
107
+
108
+ `str_replace_editor` 可见时产生固定的 schema 成本。
109
+
110
+ #### KV Cache 影响
111
+
112
+ 配置的描述与 schema 不变时前缀稳定。
113
+
114
+ ### 工具结果
115
+
116
+ #### 模型看到的内容
117
+
118
+ 查看操作返回带行号文本或浅层目录列表。调用会提供文件位置,创建/替换调用还会向展示层提供 diff 卡片。修改操作返回简洁确认。长查看结果保留前缀并追加截断提示。
119
+
120
+ #### Token 影响
121
+
122
+ 随数据变化,并受 `maxOutputChars` 与固定截断提示约束。
123
+
124
+ #### KV Cache 影响
125
+
126
+ 工具结果以追加方式位于可复用请求前缀之后。
127
+
128
+ ## 已知限制与延期工作
129
+
130
+ <a id="known-limitations-and-deferred-work"></a>
131
+
132
+
133
+ 这些限制说明编辑器工具何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用编辑器对比或任务积压。
134
+
135
+ - **操作面向 UTF-8 文本**——不支持二进制文件。
136
+ - **`str_replace` 刻意拒绝零匹配或多匹配**——它没有 `replace_all` 参数。
137
+ - **每个修改操作都会经过已挂载的策略与沙箱**——`fs/write-intent` 或 `fs/edit-intent` 解析当前会话的沙箱策略,并把强制执行委托给已挂载的文件系统与策略插件,因此未挂载它们的部署会得到无条件变更。
138
+
139
+ <a id="dev-note"></a>
140
+ ### 开发备注
141
+
142
+ <details>
143
+ <summary>维护者的工作上下文——点击展开</summary>
144
+
145
+ 无。
146
+
147
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,345 @@
1
+ import { isAbsolute } from "node:path";
2
+ import z from "@qilin-agent/schemastery";
3
+ import { FsError } from "@qilin-agent/fs";
4
+ import { sandboxDenialMarker } from "@qilin-agent/sandbox";
5
+ import { defineTool } from "@qilin-agent/tools";
6
+ //#region lib/types/index.js
7
+ /**
8
+ * Model-facing `str_replace_editor` over the Harness filesystem seam.
9
+ * @module @qilin-agent/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
+ * A null placeholder for a parameter unused by the selected command is treated as omitted. Required parameters still need values; omit \`str_replace.new_str\` rather than setting it to null when deleting a match
19
+
20
+ Notes for using the \`str_replace\` command:
21
+ * The \`old_str\` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
22
+ * 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
23
+ * The \`new_str\` parameter should contain the edited lines that should replace the \`old_str\`
24
+ `.trim();
25
+ function maybeTruncate(content, maxOutputChars) {
26
+ return content.length <= maxOutputChars ? content : content.slice(0, maxOutputChars) + TRUNCATED_MESSAGE;
27
+ }
28
+ function codepointCompare(left, right) {
29
+ return left < right ? -1 : left > right ? 1 : 0;
30
+ }
31
+ function matchOffsets(content, search) {
32
+ const offsets = [];
33
+ let offset = 0;
34
+ while (true) {
35
+ const match = content.indexOf(search, offset);
36
+ if (match < 0) return offsets;
37
+ offsets.push(match);
38
+ offset = match + search.length;
39
+ }
40
+ }
41
+ function lineNumbersAt(content, offsets) {
42
+ let line = 1;
43
+ let cursor = 0;
44
+ return offsets.map((offset) => {
45
+ while (cursor < offset) {
46
+ if (content[cursor] === "\n") line += 1;
47
+ cursor += 1;
48
+ }
49
+ return line;
50
+ });
51
+ }
52
+ var MutationPolicy = class {
53
+ policy;
54
+ constructor(ctx) {
55
+ this.policy = ctx.fs.sandboxMode === void 0 ? void 0 : ctx.get("sandboxPolicy");
56
+ 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");
57
+ }
58
+ resolve(exec) {
59
+ return this.policy?.resolve({ ...exec.agent === void 0 ? {} : { session: exec.agent.session } });
60
+ }
61
+ mapError(error, policy) {
62
+ if (!(error instanceof FsError) || error.code !== "FS_SANDBOX_DENIED") return error;
63
+ const mode = policy.mode;
64
+ return new FsError(sandboxDenialMarker(mode), "FS_SANDBOX_DENIED", { cause: error });
65
+ }
66
+ };
67
+ async function resolveTarget(ctx, path, signal) {
68
+ if (path.trim().length === 0) throw new Error("path must be a non-empty string");
69
+ if (!isAbsolute(path)) throw new Error(`The path ${path} is not an absolute path, it should start with \`/\`. Maybe you meant /${path}?`);
70
+ return ctx.fs.resolve(path, { signal });
71
+ }
72
+ async function statExisting(ctx, target, command, exec) {
73
+ const info = await ctx.fs.stat(target, exec.signal);
74
+ if (info === void 0) {
75
+ ctx.emit("fs/observed", target, { kind: "absent" }, exec);
76
+ throw new FsError(`The path ${target.displayPath} does not exist. Please provide a valid path.`, "FS_NOT_FOUND");
77
+ }
78
+ 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");
79
+ return info;
80
+ }
81
+ function requiredForCommand(value, parameter, command, allowEmpty = true) {
82
+ if (value === void 0) throw new Error(`Parameter \`${parameter}\` is required for command: ${command}`);
83
+ if (!allowEmpty && value.length === 0) throw new Error(`Parameter \`${parameter}\` is empty for command: ${command}`);
84
+ return value;
85
+ }
86
+ function formatFileView(path, content, maxOutputChars, viewRange) {
87
+ const allLines = content.split("\n");
88
+ let lines = allLines;
89
+ let initialLine = 1;
90
+ let finalLine;
91
+ let prompt = `Here's the content of ${path} with line numbers (which has a total of ${allLines.length} lines)`;
92
+ if (viewRange !== void 0) {
93
+ const [requestedInitialLine, requestedFinalLine] = viewRange;
94
+ 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.");
95
+ initialLine = requestedInitialLine;
96
+ finalLine = requestedFinalLine;
97
+ 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}]`);
98
+ 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}\``);
99
+ 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}\``);
100
+ lines = finalLine === -1 ? allLines.slice(initialLine - 1) : allLines.slice(initialLine - 1, finalLine);
101
+ prompt += ` with view_range=[${initialLine}, ${finalLine}]`;
102
+ }
103
+ const numbered = lines.map((line, index) => `${String(initialLine + index).padStart(6, " ")} ${line}`).join("\n");
104
+ return maybeTruncate(`${prompt}:\n${numbered}\n`, maxOutputChars);
105
+ }
106
+ async function listDirectory(ctx, target, maxOutputChars, exec) {
107
+ async function visit(dir, depth) {
108
+ const entries = await ctx.fs.listDir(dir, exec.signal);
109
+ const rows = [];
110
+ for (const entry of entries.filter((candidate) => !candidate.name.startsWith(".") && candidate.name !== "node_modules" && candidate.name !== "__pycache__")) {
111
+ const type = entry.type === "directory" ? "d" : entry.type === "file" ? "f" : "?";
112
+ rows.push(`${type}\t${entry.target.displayPath}`);
113
+ if (entry.type === "directory" && depth < 2) rows.push(...await visit(entry.target, depth + 1));
114
+ }
115
+ return rows;
116
+ }
117
+ const rows = [`d\t${target.displayPath}`, ...await visit(target, 1)];
118
+ rows.sort((left, right) => {
119
+ return codepointCompare(left.slice(left.indexOf(" ") + 1), right.slice(right.indexOf(" ") + 1));
120
+ });
121
+ const listing = maybeTruncate(rows.join("\n") + "\n", maxOutputChars);
122
+ 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`;
123
+ }
124
+ async function viewPath(ctx, path, viewRange, maxOutputChars, exec) {
125
+ const target = await resolveTarget(ctx, path, exec.signal);
126
+ const info = await statExisting(ctx, target, "view", exec);
127
+ if (info.type === "directory") {
128
+ if (viewRange !== void 0) throw new Error("The `view_range` parameter is not allowed when `path` points to a directory.");
129
+ return listDirectory(ctx, target, maxOutputChars, exec);
130
+ }
131
+ if (info.type !== "file") throw new FsError(`cannot view "${target.displayPath}": not a regular file or directory`, "FS_NOT_REGULAR_FILE");
132
+ const content = await ctx.fs.readText(target, exec.signal);
133
+ ctx.emit("fs/observed", target, {
134
+ kind: "present",
135
+ version: info.version
136
+ }, exec);
137
+ return formatFileView(target.displayPath, content, maxOutputChars, viewRange);
138
+ }
139
+ async function createFile(ctx, policy, path, fileText, exec) {
140
+ const content = requiredForCommand(fileText, "file_text", "create");
141
+ const sandboxPolicy = policy.resolve(exec);
142
+ const target = await resolveTarget(ctx, path, exec.signal);
143
+ 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\`.`);
144
+ const intent = await ctx.waterfall("fs/write-intent", target, exec, () => ({ kind: "createIfAbsent" }));
145
+ let outcome;
146
+ try {
147
+ outcome = await ctx.fs.writeText(target, content, intent, exec.signal, sandboxPolicy);
148
+ } catch (error) {
149
+ throw policy.mapError(error, sandboxPolicy);
150
+ }
151
+ ctx.emit("fs/observed", target, {
152
+ kind: "present",
153
+ version: outcome.version
154
+ }, exec);
155
+ return `New file created successfully at: ${target.displayPath}`;
156
+ }
157
+ async function replaceInFile(ctx, policy, path, oldStr, newStr, exec) {
158
+ if (newStr === null) throw new Error("Parameter `new_str` must be omitted or contain a string for command: str_replace");
159
+ const sandboxPolicy = policy.resolve(exec);
160
+ const target = await resolveTarget(ctx, path, exec.signal);
161
+ const intent = await ctx.waterfall("fs/edit-intent", target, exec, () => void 0);
162
+ const oldValue = requiredForCommand(oldStr, "old_str", "str_replace", false);
163
+ const newValue = newStr ?? "";
164
+ const info = await statExisting(ctx, target, "str_replace", exec);
165
+ if (info.type !== "file") throw new FsError(`cannot edit "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
166
+ const before = await ctx.fs.readText(target, exec.signal);
167
+ const offsets = matchOffsets(before, oldValue);
168
+ const offset = offsets[0];
169
+ 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");
170
+ 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");
171
+ let outcome;
172
+ try {
173
+ outcome = await ctx.fs.writeText(target, before.slice(0, offset) + newValue + before.slice(offset + oldValue.length), intent === void 0 ? {
174
+ kind: "replaceIfVersion",
175
+ version: info.version
176
+ } : {
177
+ kind: "replaceIfVersion",
178
+ version: intent.version
179
+ }, exec.signal, sandboxPolicy);
180
+ } catch (error) {
181
+ throw policy.mapError(error, sandboxPolicy);
182
+ }
183
+ ctx.emit("fs/observed", target, {
184
+ kind: "present",
185
+ version: outcome.version
186
+ }, exec);
187
+ return `The file ${target.displayPath} has been edited successfully.`;
188
+ }
189
+ async function insertInFile(ctx, policy, path, insertLine, newStr, exec) {
190
+ if (insertLine === void 0) throw new Error("Parameter `insert_line` is required for command: insert");
191
+ const value = requiredForCommand(newStr, "new_str", "insert");
192
+ const sandboxPolicy = policy.resolve(exec);
193
+ const target = await resolveTarget(ctx, path, exec.signal);
194
+ const intent = await ctx.waterfall("fs/edit-intent", target, exec, () => void 0);
195
+ const info = await statExisting(ctx, target, "insert", exec);
196
+ if (info.type !== "file") throw new FsError(`cannot insert into "${target.displayPath}": not a regular file`, "FS_NOT_REGULAR_FILE");
197
+ const lines = (await ctx.fs.readText(target, exec.signal)).split("\n");
198
+ 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}]`);
199
+ const after = [
200
+ ...lines.slice(0, insertLine),
201
+ ...value.split("\n"),
202
+ ...lines.slice(insertLine)
203
+ ].join("\n");
204
+ const expected = intent === void 0 ? {
205
+ kind: "replaceIfVersion",
206
+ version: info.version
207
+ } : {
208
+ kind: "replaceIfVersion",
209
+ version: intent.version
210
+ };
211
+ let outcome;
212
+ try {
213
+ outcome = await ctx.fs.writeText(target, after, expected, exec.signal, sandboxPolicy);
214
+ } catch (error) {
215
+ throw policy.mapError(error, sandboxPolicy);
216
+ }
217
+ ctx.emit("fs/observed", target, {
218
+ kind: "present",
219
+ version: outcome.version
220
+ }, exec);
221
+ return `The file ${target.displayPath} has been edited successfully.`;
222
+ }
223
+ function presentEditorCall(args) {
224
+ switch (args.command) {
225
+ case "view": return {
226
+ card: "generic",
227
+ title: `view ${args.path}`,
228
+ kind: "read",
229
+ locations: [{ path: args.path }]
230
+ };
231
+ case "create": return {
232
+ card: "diff",
233
+ title: `create ${args.path}`,
234
+ diffs: [{
235
+ path: args.path,
236
+ oldText: null,
237
+ newText: args.file_text ?? ""
238
+ }],
239
+ locations: [{ path: args.path }]
240
+ };
241
+ case "str_replace": return {
242
+ card: "diff",
243
+ title: `str_replace ${args.path}`,
244
+ diffs: [{
245
+ path: args.path,
246
+ oldText: args.old_str ?? null,
247
+ newText: args.new_str ?? ""
248
+ }],
249
+ locations: [{ path: args.path }]
250
+ };
251
+ case "insert": return {
252
+ card: "generic",
253
+ title: `insert ${args.path}`,
254
+ kind: "edit",
255
+ locations: [{
256
+ path: args.path,
257
+ ...args.insert_line === void 0 || args.insert_line === null ? {} : { line: Math.max(1, args.insert_line + 1) }
258
+ }]
259
+ };
260
+ }
261
+ }
262
+ /** Register the model-facing `str_replace_editor` tool. */
263
+ function registerStrReplaceEditor(ctx, config) {
264
+ const policy = new MutationPolicy(ctx);
265
+ ctx.tools.register(defineTool({
266
+ name: "str_replace_editor",
267
+ description: config.description,
268
+ parameters: {
269
+ command: {
270
+ type: "string",
271
+ required: true,
272
+ enum: [
273
+ "view",
274
+ "create",
275
+ "str_replace",
276
+ "insert"
277
+ ],
278
+ description: "The commands to run. Allowed options are: `view`, `create`, `str_replace`, `insert`."
279
+ },
280
+ path: {
281
+ type: "string",
282
+ required: true,
283
+ description: "Absolute path to file or directory, e.g. `/repo/file.py` or `/repo`."
284
+ },
285
+ file_text: {
286
+ oneOf: [{ type: "string" }, { type: "null" }],
287
+ description: "Required string parameter of `create` command, with the content of the file to be created. A null placeholder is treated as omitted by commands that do not use this parameter."
288
+ },
289
+ insert_line: {
290
+ oneOf: [{ type: "integer" }, { type: "null" }],
291
+ description: "Required integer parameter of `insert` command. The `new_str` will be inserted AFTER the line `insert_line` of `path`. A null placeholder is treated as omitted by commands that do not use this parameter."
292
+ },
293
+ new_str: {
294
+ oneOf: [{ type: "string" }, { type: "null" }],
295
+ description: "Optional string parameter of `str_replace` command containing the new string (if omitted, no string will be added). Required string parameter of `insert` command containing the string to insert. A null placeholder is accepted only by commands that do not use this parameter."
296
+ },
297
+ old_str: {
298
+ oneOf: [{ type: "string" }, { type: "null" }],
299
+ description: "Required string parameter of `str_replace` command containing the string in `path` to replace. A null placeholder is treated as omitted by commands that do not use this parameter."
300
+ },
301
+ view_range: {
302
+ oneOf: [{
303
+ type: "array",
304
+ items: { type: "integer" }
305
+ }, { type: "null" }],
306
+ description: "Optional parameter of `view` command when `path` points to a file. If omitted or null, 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."
307
+ }
308
+ },
309
+ output: {
310
+ schema: { type: "string" },
311
+ render: (_args, value) => [{
312
+ type: "text",
313
+ text: value
314
+ }]
315
+ },
316
+ async execute(args, exec) {
317
+ switch (args.command) {
318
+ case "view": return viewPath(ctx, args.path, args.view_range ?? void 0, config.maxOutputChars, exec);
319
+ case "create": return createFile(ctx, policy, args.path, args.file_text ?? void 0, exec);
320
+ case "str_replace": return replaceInFile(ctx, policy, args.path, args.old_str ?? void 0, args.new_str, exec);
321
+ case "insert": return insertInFile(ctx, policy, args.path, args.insert_line ?? void 0, args.new_str ?? void 0, exec);
322
+ }
323
+ },
324
+ presentCall: presentEditorCall
325
+ }));
326
+ }
327
+ const name = "tool-str-replace-editor";
328
+ const inject = ["tools", "fs"];
329
+ /** Runtime configuration schema for the string-replacement editor tool. */
330
+ const Config = z.object({
331
+ maxOutputChars: z.number().default(16e3),
332
+ description: z.string().default(DEFAULT_DESCRIPTION)
333
+ });
334
+ /** Register one `str_replace_editor` tool over `ctx.fs`. */
335
+ function apply(ctx, config) {
336
+ const resolved = {
337
+ maxOutputChars: config.maxOutputChars ?? 16e3,
338
+ description: config.description ?? DEFAULT_DESCRIPTION
339
+ };
340
+ if (!Number.isSafeInteger(resolved.maxOutputChars) || resolved.maxOutputChars <= 0) throw new Error("tool-str-replace-editor: maxOutputChars must be a positive safe integer");
341
+ if (resolved.description.trim().length === 0) throw new Error("tool-str-replace-editor: description must be non-empty");
342
+ registerStrReplaceEditor(ctx, resolved);
343
+ }
344
+ //#endregion
345
+ export { Config, apply, inject, name };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Model-facing `str_replace_editor` over the Harness filesystem seam.
3
+ * @module @qilin-agent/tool-str-replace-editor
4
+ */
5
+ import type { Context } from '@qilin-agent/kylin';
6
+ import z from '@qilin-agent/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
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@qilin-agent/tool-str-replace-editor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Model-facing view, create, literal replace, and line insert tool over the Harness filesystem service",
4
+ "version": "3.1.3",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kkutysllb/QiLin.git",
11
+ "directory": "packages/fs/tool-str-replace-editor"
12
+ },
13
+ "engines": {
14
+ "node": "^22.19.0 || >=24.0.0"
15
+ },
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "types": "lib/types/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./lib/types/index.d.ts",
22
+ "default": "./lib/index.js"
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "files": [
27
+ "lib/index.js",
28
+ "lib/types/**/*.d.ts"
29
+ ],
30
+ "license": "MIT",
31
+ "peerDependencies": {
32
+ "@qilin-agent/sandbox-policy": "3.1.3",
33
+ "@qilin-agent/fs": "3.1.3",
34
+ "@qilin-agent/sandbox": "3.1.3",
35
+ "@qilin-agent/kylin": "~4.0.3",
36
+ "@qilin-agent/tools": "3.1.3"
37
+ },
38
+ "dependencies": {
39
+ "@qilin-agent/schemastery": "~3.18.3"
40
+ },
41
+ "devDependencies": {
42
+ "@qilin-agent/agent": "3.1.3",
43
+ "@qilin-agent/agent-loop-testkit": "3.1.3",
44
+ "@qilin-agent/fs": "3.1.3",
45
+ "@qilin-agent/fs-local": "3.1.3",
46
+ "@qilin-agent/fs-observation-policy": "3.1.3",
47
+ "@qilin-agent/fs-sandbox": "3.1.3",
48
+ "@qilin-agent/sandbox": "3.1.3",
49
+ "@qilin-agent/sandbox-policy": "3.1.3",
50
+ "@qilin-agent/session": "3.1.3",
51
+ "@qilin-agent/system-prompt": "3.1.3",
52
+ "@qilin-agent/llm": "3.1.3",
53
+ "@qilin-agent/tools": "3.1.3",
54
+ "@qilin-agent/session-projection": "3.1.3",
55
+ "@qilin-agent/kylin": "~4.0.3"
56
+ }
6
57
  }