@hydraharness/harness-tool-str-replace-editor 0.0.0-stage → 0.1.1-rc.7

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 DeepSeek
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.
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # @hydraharness/harness-tool-str-replace-editor
2
2
 
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.
3
+ 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.
4
+
5
+ ## Config
6
+
7
+ | Key | Default | Meaning |
8
+ |---|---:|---|
9
+ | `maxOutputChars` | `16000` | Prefix characters retained for file and directory views. |
10
+ | `description` | Editor command guide | Model-facing tool description. |
11
+
12
+ ## Tool
13
+
14
+ 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.
15
+
16
+ ## Model Experience
17
+
18
+ ### Tool schema
19
+
20
+ #### What the model sees
21
+
22
+ The generated [`str_replace_editor` schema](../../../docs/tool-catalog.md#hydraharness-tool-str-replace-editor), including the configured `description`. The plugin contributes no standalone system-prompt section.
23
+
24
+ #### Token effect
25
+
26
+ Fixed schema cost while `str_replace_editor` is visible.
27
+
28
+ #### KV Cache effect
29
+
30
+ Prefix-stable while the configured description and schema remain unchanged.
31
+
32
+ ### Tool results
33
+
34
+ #### What the model sees
35
+
36
+ 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.
37
+
38
+ #### Token effect
39
+
40
+ Data-dependent and bounded by `maxOutputChars` plus the fixed clipping notice.
41
+
42
+ #### KV Cache effect
43
+
44
+ Append-only tool results follow the reusable request prefix.
45
+
46
+ ## Known Limitations and Deferred Work
47
+
48
+ - Operations target UTF-8 text; binary files are unsupported.
49
+ - `str_replace` intentionally rejects zero or multiple matches and has no `replace_all` argument.
50
+ - 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/lib/index.js ADDED
@@ -0,0 +1,345 @@
1
+ import { isAbsolute } from "node:path";
2
+ import z from "@hydraharness/schemastery";
3
+ import { FsError } from "@hydraharness/harness-fs";
4
+ import { sandboxDenialMarker } from "@hydraharness/harness-sandbox";
5
+ import { defineTool } from "@hydraharness/harness-tools";
6
+ //#region lib/types/index.js
7
+ /**
8
+ * Model-facing `str_replace_editor` over the Harness filesystem seam.
9
+ * @module @hydraharness/harness-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,23 @@
1
+ //#region lib/types/invariant.js
2
+ /**
3
+ * Package-owned invariant companion for `@hydraharness/harness-tool-str-replace-editor`.
4
+ * @module @hydraharness/harness-tool-str-replace-editor/invariant
5
+ */
6
+ const PACKAGE_NAME = "@hydraharness/harness-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 @hydraharness/harness-tool-str-replace-editor
4
+ */
5
+ import type { Context } from '@hydraharness/cordis';
6
+ import z from '@hydraharness/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 `@hydraharness/harness-tool-str-replace-editor`.
3
+ * @module @hydraharness/harness-tool-str-replace-editor/invariant
4
+ */
5
+ import type { Context } from '@hydraharness/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 CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@hydraharness/harness-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
+ "hydra": {
5
+ "plugin": {
6
+ "application": "Let the agent view files, create files, replace exact text, or insert lines."
7
+ }
8
+ },
9
+ "version": "0.1.1-rc.7",
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/MaiHongPhong1902/Hydra-Harness.git",
16
+ "directory": "packages/fs/tool-str-replace-editor"
17
+ },
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "types": "lib/types/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/types/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ },
26
+ "./invariant": {
27
+ "types": "./lib/types/invariant.d.ts",
28
+ "default": "./lib/invariant.js"
29
+ },
30
+ "./package.json": "./package.json"
31
+ },
32
+ "files": [
33
+ "lib/index.js",
34
+ "lib/invariant.js",
35
+ "lib/types/**/*.d.ts"
36
+ ],
37
+ "license": "MIT",
38
+ "peerDependencies": {
39
+ "@hydraharness/harness-fs": "^0.1.1-rc.7",
40
+ "@hydraharness/harness-invariants": "^0.1.1-rc.7",
41
+ "@hydraharness/harness-sandbox": "^0.1.1-rc.7",
42
+ "@hydraharness/harness-sandbox-policy": "^0.1.1-rc.7",
43
+ "@hydraharness/cordis": "^4.0.2",
44
+ "@hydraharness/harness-tools": "^0.1.1-rc.7"
45
+ },
46
+ "dependencies": {
47
+ "@hydraharness/schemastery": "^3.18.2"
48
+ },
49
+ "devDependencies": {
50
+ "@hydraharness/harness-agent": "^0.1.1-rc.7",
51
+ "@hydraharness/harness-fs": "^0.1.1-rc.7",
52
+ "@hydraharness/harness-fs-local": "^0.1.1-rc.7",
53
+ "@hydraharness/harness-fs-sandbox": "^0.1.1-rc.7",
54
+ "@hydraharness/harness-fs-observation-policy": "^0.1.1-rc.7",
55
+ "@hydraharness/harness-llm": "^0.1.1-rc.7",
56
+ "@hydraharness/harness-invariants": "^0.1.1-rc.7",
57
+ "@hydraharness/harness-sandbox": "^0.1.1-rc.7",
58
+ "@hydraharness/harness-system-prompt": "^0.1.1-rc.7",
59
+ "@hydraharness/harness-sandbox-policy": "^0.1.1-rc.7",
60
+ "@hydraharness/harness-session": "^0.1.1-rc.7",
61
+ "@hydraharness/harness-tools": "^0.1.1-rc.7",
62
+ "@hydraharness/cordis": "^4.0.2"
63
+ }
6
64
  }