@osovv/vv-opencode 1.2.7 → 1.2.10

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +63 -3
  3. package/dist/commands/patch-provider.js +2 -0
  4. package/dist/commands/patch-provider.js.map +1 -1
  5. package/dist/commands/plugin-list.js +8 -3
  6. package/dist/commands/plugin-list.js.map +1 -1
  7. package/dist/commands/plugin-toggle.js +8 -1
  8. package/dist/commands/plugin-toggle.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +5 -4
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/opencode.js +3 -0
  13. package/dist/lib/opencode.js.map +1 -1
  14. package/dist/lib/plugin-toggle-config.d.ts +44 -2
  15. package/dist/lib/plugin-toggle-config.js +132 -11
  16. package/dist/lib/plugin-toggle-config.js.map +1 -1
  17. package/dist/lib/vvoc-config.d.ts +80 -2
  18. package/dist/lib/vvoc-config.js +62 -1
  19. package/dist/lib/vvoc-config.js.map +1 -1
  20. package/dist/plugins/hashline-edit/diff-summary.d.ts +7 -0
  21. package/dist/plugins/hashline-edit/diff-summary.js +79 -0
  22. package/dist/plugins/hashline-edit/diff-summary.js.map +1 -0
  23. package/dist/plugins/hashline-edit/edit-operation-primitives.js +22 -3
  24. package/dist/plugins/hashline-edit/edit-operation-primitives.js.map +1 -1
  25. package/dist/plugins/hashline-edit/index.js +315 -76
  26. package/dist/plugins/hashline-edit/index.js.map +1 -1
  27. package/dist/plugins/hashline-edit/normalize-edits.js +29 -25
  28. package/dist/plugins/hashline-edit/normalize-edits.js.map +1 -1
  29. package/dist/plugins/hashline-edit/replace-engine.d.ts +24 -0
  30. package/dist/plugins/hashline-edit/replace-engine.js +246 -0
  31. package/dist/plugins/hashline-edit/replace-engine.js.map +1 -0
  32. package/dist/plugins/hashline-edit/routing.d.ts +21 -0
  33. package/dist/plugins/hashline-edit/routing.js +105 -0
  34. package/dist/plugins/hashline-edit/routing.js.map +1 -0
  35. package/dist/plugins/hashline-edit/session-state.d.ts +23 -0
  36. package/dist/plugins/hashline-edit/session-state.js +70 -0
  37. package/dist/plugins/hashline-edit/session-state.js.map +1 -0
  38. package/dist/plugins/hashline-edit/str-replace-editor.d.ts +50 -0
  39. package/dist/plugins/hashline-edit/str-replace-editor.js +285 -0
  40. package/dist/plugins/hashline-edit/str-replace-editor.js.map +1 -0
  41. package/dist/plugins/hashline-edit/tool-description.d.ts +2 -1
  42. package/dist/plugins/hashline-edit/tool-description.js +31 -10
  43. package/dist/plugins/hashline-edit/tool-description.js.map +1 -1
  44. package/dist/plugins/hashline-edit/validation.d.ts +1 -0
  45. package/dist/plugins/hashline-edit/validation.js +13 -3
  46. package/dist/plugins/hashline-edit/validation.js.map +1 -1
  47. package/dist/plugins/tool-history-compaction/config.d.ts +28 -0
  48. package/dist/plugins/tool-history-compaction/config.js +133 -0
  49. package/dist/plugins/tool-history-compaction/config.js.map +1 -0
  50. package/dist/plugins/tool-history-compaction/index.d.ts +2 -0
  51. package/dist/plugins/tool-history-compaction/index.js +35 -0
  52. package/dist/plugins/tool-history-compaction/index.js.map +1 -0
  53. package/dist/plugins/tool-history-compaction/prune.d.ts +22 -0
  54. package/dist/plugins/tool-history-compaction/prune.js +70 -0
  55. package/dist/plugins/tool-history-compaction/prune.js.map +1 -0
  56. package/dist/plugins/tool-history-compaction/read-slim.d.ts +27 -0
  57. package/dist/plugins/tool-history-compaction/read-slim.js +91 -0
  58. package/dist/plugins/tool-history-compaction/read-slim.js.map +1 -0
  59. package/dist/plugins/tool-history-compaction/retention.d.ts +10 -0
  60. package/dist/plugins/tool-history-compaction/retention.js +45 -0
  61. package/dist/plugins/tool-history-compaction/retention.js.map +1 -0
  62. package/dist/plugins/tool-history-compaction/transform.d.ts +15 -0
  63. package/dist/plugins/tool-history-compaction/transform.js +93 -0
  64. package/dist/plugins/tool-history-compaction/transform.js.map +1 -0
  65. package/package.json +5 -1
  66. package/schemas/vvoc/v3.json +43 -2
@@ -1,8 +1,8 @@
1
1
  // FILE: src/plugins/hashline-edit/edit-operation-primitives.ts
2
- // VERSION: 0.2.0
2
+ // VERSION: 0.3.0
3
3
  // START_MODULE_CONTRACT
4
4
  // PURPOSE: Apply validated hashline edit operations literally to an in-memory file snapshot one mutation at a time.
5
- // SCOPE: Single-line replace, range replace, anchored insert-before/after with exact-match echo trimming, and BOF/EOF insert helpers that preserve trailing-newline sentinels.
5
+ // SCOPE: Single-line replace, range replace, anchored insert-before/after with exact-match echo trimming, neighbor-duplication warnings for insertions, and BOF/EOF insert helpers that preserve trailing-newline sentinels.
6
6
  // DEPENDS: [src/plugins/hashline-edit/edit-text-normalization.ts, src/plugins/hashline-edit/validation.ts]
7
7
  // LINKS: [M-PLUGIN-HASHLINE-EDIT]
8
8
  // ROLE: RUNTIME
@@ -20,13 +20,24 @@
20
20
  // END_MODULE_MAP
21
21
  //
22
22
  // START_CHANGE_SUMMARY
23
- // LAST_CHANGE: [v0.2.0 - Removed autocorrect and indent restoration, switched echo trimming to exact-match with warnings, and fixed EOF append inserting a phantom blank line before the trailing newline.]
23
+ // LAST_CHANGE: [v0.3.0 - Added visible warnings when append/prepend payloads duplicate the lines already adjacent to the anchor, applied literally.]
24
24
  // END_CHANGE_SUMMARY
25
25
  import { stripExactBoundaryEchoes, stripExactInsertBeforeEcho, stripExactInsertEcho, toNewLines, } from "./edit-text-normalization.js";
26
26
  import { parseLineRef, validateLineRef } from "./validation.js";
27
27
  function shouldValidate(options) {
28
28
  return options?.skipValidation !== true;
29
29
  }
30
+ function linesEqual(left, right) {
31
+ if (left.length !== right.length || left.length === 0) {
32
+ return false;
33
+ }
34
+ for (let i = 0; i < left.length; i += 1) {
35
+ if (left[i] !== right[i]) {
36
+ return false;
37
+ }
38
+ }
39
+ return true;
40
+ }
30
41
  export function applySetLine(lines, anchor, newText, options) {
31
42
  if (shouldValidate(options)) {
32
43
  validateLineRef(lines, anchor);
@@ -74,6 +85,10 @@ export function applyInsertAfter(lines, anchor, text, options) {
74
85
  if (trimmed.lines.length === 0) {
75
86
  throw new Error(`append (anchored) requires non-empty text for ${anchor}`);
76
87
  }
88
+ const following = lines.slice(line, line + trimmed.lines.length);
89
+ if (linesEqual(trimmed.lines, following)) {
90
+ options?.onWarning?.(`append at line ${line}: payload duplicates the ${trimmed.lines.length} line(s) already following the anchor; applying literally creates a duplicate block`);
91
+ }
77
92
  result.splice(line, 0, ...trimmed.lines);
78
93
  return result;
79
94
  }
@@ -90,6 +105,10 @@ export function applyInsertBefore(lines, anchor, text, options) {
90
105
  if (trimmed.lines.length === 0) {
91
106
  throw new Error(`prepend (anchored) requires non-empty text for ${anchor}`);
92
107
  }
108
+ const preceding = lines.slice(line - 1 - trimmed.lines.length, line - 1);
109
+ if (linesEqual(trimmed.lines, preceding)) {
110
+ options?.onWarning?.(`prepend at line ${line}: payload duplicates the ${trimmed.lines.length} line(s) already preceding the anchor; applying literally creates a duplicate block`);
111
+ }
93
112
  result.splice(line - 1, 0, ...trimmed.lines);
94
113
  return result;
95
114
  }
@@ -1 +1 @@
1
- {"version":3,"file":"edit-operation-primitives.js","sourceRoot":"","sources":["../../../src/plugins/hashline-edit/edit-operation-primitives.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"edit-operation-primitives.js","sourceRoot":"","sources":["../../../src/plugins/hashline-edit/edit-operation-primitives.ts"],"names":[],"mappings":"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"}
@@ -1,31 +1,36 @@
1
1
  // FILE: src/plugins/hashline-edit/index.ts
2
- // VERSION: 0.7.0
2
+ // VERSION: 0.8.0
3
3
  // START_MODULE_CONTRACT
4
- // PURPOSE: Override OpenCode's default `edit` tool with a hash-anchored edit implementation and hash-aware read output with context-anchored hash references.
5
- // SCOPE: Hashline-backed edit tool registration, read-output transformation with anchor hashes, anchor validation, literal file mutation execution, bounded post-edit diff feedback, and success metadata emission.
6
- // DEPENDS: [@opencode-ai/plugin, src/lib/config-layers.ts, src/lib/plugin-toggle-config.ts, src/plugins/hashline-edit/edit-operations.ts, src/plugins/hashline-edit/file-text-canonicalization.ts, src/plugins/hashline-edit/hash-computation.ts, src/plugins/hashline-edit/normalize-edits.ts, src/plugins/hashline-edit/tool-description.ts, src/plugins/hashline-edit/validation.ts]
4
+ // PURPOSE: Route per-model edit tooling: register hashline_edit, replace-profile edit, and dsh str_replace_editor; resolve the session edit mode from vvoc routing config; expose only the profile tool to each model; and transform read output with anchors for hashline sessions.
5
+ // SCOPE: Routing config loading, session model/file caches, chat.message tool-visibility mutation, tool.execute.before safety net, routed read transformation, hashline/replace/str_replace_editor execution, bounded post-edit diff feedback, and editMode telemetry metadata.
6
+ // DEPENDS: [@opencode-ai/plugin, node:fs/promises, node:path, src/lib/config-layers.ts, src/plugins/hashline-edit/diff-summary.ts, src/plugins/hashline-edit/edit-operations.ts, src/plugins/hashline-edit/file-text-canonicalization.ts, src/plugins/hashline-edit/hash-computation.ts, src/plugins/hashline-edit/normalize-edits.ts, src/plugins/hashline-edit/replace-engine.ts, src/plugins/hashline-edit/routing.ts, src/plugins/hashline-edit/session-state.ts, src/plugins/hashline-edit/str-replace-editor.ts, src/plugins/hashline-edit/tool-description.ts, src/plugins/hashline-edit/validation.ts]
7
7
  // LINKS: [M-PLUGIN-HASHLINE-EDIT]
8
8
  // ROLE: RUNTIME
9
9
  // MAP_MODE: EXPORTS
10
10
  // END_MODULE_CONTRACT
11
11
  //
12
12
  // START_MODULE_MAP
13
- // HashlineEditPlugin - Registers the hash-anchored `edit` tool override and post-read output enhancer.
13
+ // HashlineEditPlugin - Registers routed edit tools (hashline_edit, edit, str_replace_editor), per-model tool visibility, and the routed read-output enhancer.
14
14
  // END_MODULE_MAP
15
15
  //
16
16
  // START_CHANGE_SUMMARY
17
- // LAST_CHANGE: [v0.7.0 - Successful edits now report line deltas, apply warnings, and a bounded diff in the tool output; application is literal.]
17
+ // LAST_CHANGE: [v0.8.0 - Added per-model edit-format routing with native replace and dsh str_replace_editor profiles, per-model tool visibility via chat.message, safety-net rejection, and editMode telemetry.]
18
18
  // END_CHANGE_SUMMARY
19
19
  import { tool } from "@opencode-ai/plugin";
20
- import { resolve } from "node:path";
20
+ import { dirname, resolve } from "node:path";
21
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
22
+ import { findFirstChangedLine, summarizeEditDiff } from "./diff-summary.js";
21
23
  import { applyHashlineEditsWithReport } from "./edit-operations.js";
22
24
  import { canonicalizeFileText, restoreFileText } from "./file-text-canonicalization.js";
23
25
  import { computeAnchorHash, computeLineHash } from "./hash-computation.js";
24
26
  import { normalizeHashlineEdits } from "./normalize-edits.js";
25
- import { HASHLINE_EDIT_DESCRIPTION } from "./tool-description.js";
27
+ import { applyReplaceEdit, formatReplaceSuccess } from "./replace-engine.js";
28
+ import { parseHashlineEditPluginEntry, resolveEditMode, } from "./routing.js";
29
+ import { SessionFileCache, SessionModelCache } from "./session-state.js";
30
+ import { STR_REPLACE_EDITOR_DESCRIPTION, StrReplaceEditor, } from "./str-replace-editor.js";
31
+ import { HASHLINE_EDIT_DESCRIPTION, REPLACE_EDIT_DESCRIPTION } from "./tool-description.js";
26
32
  import { HashlineMismatchError } from "./validation.js";
27
33
  import { loadVvocConfig } from "../../lib/config-layers.js";
28
- import { isVvocPluginEnabled } from "../../lib/plugin-toggle-config.js";
29
34
  const z = tool.schema;
30
35
  const CONTENT_OPEN_TAG = "<content>";
31
36
  const CONTENT_CLOSE_TAG = "</content>";
@@ -34,65 +39,85 @@ const FILE_CLOSE_TAG = "</file>";
34
39
  const OPENCODE_LINE_TRUNCATION_SUFFIX = "... (line truncated to 2000 chars)";
35
40
  const COLON_READ_LINE_PATTERN = /^\s*(\d+): ?(.*)$/;
36
41
  const PIPE_READ_LINE_PATTERN = /^\s*(\d+)\| ?(.*)$/;
37
- function canCreateFromMissingFile(edits) {
38
- if (edits.length === 0) {
39
- return false;
40
- }
41
- return edits.every((edit) => (edit.op === "append" || edit.op === "prepend") && edit.pos === undefined);
42
+ // START_BLOCK_ROUTING_CONSTANTS
43
+ const EDIT_TYPE_TOOLS = ["hashline_edit", "edit", "str_replace_editor"];
44
+ function isEditTypeTool(toolName) {
45
+ return EDIT_TYPE_TOOLS.includes(toolName);
42
46
  }
43
- function findFirstChangedLine(beforeContent, afterContent) {
44
- const beforeLines = beforeContent.split("\n");
45
- const afterLines = afterContent.split("\n");
46
- const maxLength = Math.max(beforeLines.length, afterLines.length);
47
- for (let index = 0; index < maxLength; index += 1) {
48
- if ((beforeLines[index] ?? "") !== (afterLines[index] ?? "")) {
49
- return index + 1;
50
- }
47
+ function visibleToolsForMode(mode) {
48
+ switch (mode) {
49
+ case "hashline":
50
+ return ["hashline_edit"];
51
+ case "replace":
52
+ return ["edit"];
53
+ case "str_replace_editor":
54
+ return ["str_replace_editor"];
55
+ case "passthrough":
56
+ return [];
51
57
  }
52
- return undefined;
53
58
  }
54
- const DIFF_CONTEXT_LINES = 2;
55
- const DIFF_MAX_RENDERED_LINES = 40;
56
- function summarizeEditDiff(beforeContent, afterContent) {
57
- const before = beforeContent.length === 0 ? [] : beforeContent.split("\n");
58
- const after = afterContent.length === 0 ? [] : afterContent.split("\n");
59
- const minLen = Math.min(before.length, after.length);
60
- let prefix = 0;
61
- while (prefix < minLen && before[prefix] === after[prefix]) {
62
- prefix += 1;
63
- }
64
- let suffix = 0;
65
- while (suffix < minLen - prefix &&
66
- before[before.length - 1 - suffix] === after[after.length - 1 - suffix]) {
67
- suffix += 1;
68
- }
69
- const removed = before.slice(prefix, before.length - suffix);
70
- const added = after.slice(prefix, after.length - suffix);
71
- if (removed.length === 0 && added.length === 0) {
72
- return { additions: 0, deletions: 0, rendered: [] };
73
- }
74
- const rendered = [`@@ changed lines ${prefix + 1} @@`];
75
- const contextStart = Math.max(0, prefix - DIFF_CONTEXT_LINES);
76
- for (let i = contextStart; i < prefix; i += 1) {
77
- rendered.push(` ${before[i]}`);
78
- }
79
- for (const line of removed) {
80
- rendered.push(`- ${line}`);
81
- }
82
- for (const line of added) {
83
- rendered.push(`+ ${line}`);
59
+ // START_BLOCK_FS_HELPERS
60
+ async function statSnapshot(filePath) {
61
+ try {
62
+ const info = await stat(filePath);
63
+ if (!info.isFile()) {
64
+ return undefined;
65
+ }
66
+ return { mtimeMs: info.mtimeMs, size: info.size };
84
67
  }
85
- const tailStart = before.length - suffix;
86
- const contextEnd = Math.min(before.length, tailStart + DIFF_CONTEXT_LINES);
87
- for (let i = tailStart; i < contextEnd; i += 1) {
88
- rendered.push(` ${before[i]}`);
68
+ catch {
69
+ return undefined;
89
70
  }
90
- if (rendered.length > DIFF_MAX_RENDERED_LINES) {
91
- const kept = rendered.slice(0, DIFF_MAX_RENDERED_LINES - 1);
92
- kept.push(`… (${rendered.length - kept.length} more diff lines)`);
93
- return { additions: added.length, deletions: removed.length, rendered: kept };
71
+ }
72
+ function createNodeStrReplaceFs() {
73
+ return {
74
+ async exists(path) {
75
+ try {
76
+ await stat(path);
77
+ return true;
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ },
83
+ async isDirectory(path) {
84
+ try {
85
+ return (await stat(path)).isDirectory();
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ },
91
+ async readText(path) {
92
+ return readFile(path, "utf8");
93
+ },
94
+ async writeText(path, content) {
95
+ await writeFile(path, content, "utf8");
96
+ },
97
+ async listDir(path) {
98
+ const entries = await readdir(path, { withFileTypes: true });
99
+ return entries.map((entry) => {
100
+ const type = entry.isDirectory() ? "directory" : "file";
101
+ return { name: entry.name, type };
102
+ });
103
+ },
104
+ async stat(path) {
105
+ try {
106
+ const info = await stat(path);
107
+ return { mtimeMs: info.mtimeMs, size: info.isFile() ? info.size : 0 };
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ },
113
+ };
114
+ }
115
+ // END_BLOCK_FS_HELPERS
116
+ function canCreateFromMissingFile(edits) {
117
+ if (edits.length === 0) {
118
+ return false;
94
119
  }
95
- return { additions: added.length, deletions: removed.length, rendered };
120
+ return edits.every((edit) => (edit.op === "append" || edit.op === "prepend") && edit.pos === undefined);
96
121
  }
97
122
  function publishSuccessMetadata(args) {
98
123
  args.context.metadata({
@@ -104,6 +129,9 @@ function publishSuccessMetadata(args) {
104
129
  noopEdits: args.noopEdits,
105
130
  deduplicatedEdits: args.deduplicatedEdits,
106
131
  firstChangedLine: findFirstChangedLine(args.beforeContent, args.afterContent),
132
+ editMode: args.telemetry.editMode,
133
+ providerID: args.telemetry.providerID,
134
+ modelID: args.telemetry.modelID,
107
135
  filediff: {
108
136
  file: args.filePath,
109
137
  path: args.filePath,
@@ -298,7 +326,8 @@ function transformReadOutput(output, sourceLines) {
298
326
  const result = formatReadLines(parsedLines, lines, sourceMatchesVisibleRows(sourceLines, parsedLines) ? sourceLines : undefined);
299
327
  return result.join("\n");
300
328
  }
301
- async function executeHashlineEdit(args, context) {
329
+ // START_BLOCK_HASHLINE_EXECUTE
330
+ async function executeHashlineEdit(args, context, telemetry) {
302
331
  try {
303
332
  const { filePath, rename, delete: deleteMode } = args;
304
333
  if (deleteMode && rename) {
@@ -357,6 +386,7 @@ async function executeHashlineEdit(args, context) {
357
386
  afterContent: canonicalNewContent,
358
387
  noopEdits: applyResult.noopEdits,
359
388
  deduplicatedEdits: applyResult.deduplicatedEdits,
389
+ telemetry,
360
390
  });
361
391
  const diffSummary = summarizeEditDiff(oldEnvelope.content, canonicalNewContent);
362
392
  const firstChangedLine = findFirstChangedLine(oldEnvelope.content, canonicalNewContent);
@@ -382,13 +412,173 @@ async function executeHashlineEdit(args, context) {
382
412
  return `Error: ${message}`;
383
413
  }
384
414
  }
415
+ // END_BLOCK_HASHLINE_EXECUTE
416
+ // START_BLOCK_REPLACE_EXECUTE
417
+ async function executeReplaceEdit(args, context, sessionID, fileCache, telemetry) {
418
+ const filePath = args.filePath ?? args.file_path;
419
+ if (!filePath || filePath.trim().length === 0) {
420
+ return "Error: filePath is required (filePath or file_path)";
421
+ }
422
+ const oldString = args.oldString ?? args.old_string ?? "";
423
+ const newString = args.newString ?? args.new_string ?? "";
424
+ const replaceAll = args.replaceAll ?? args.replace_all ?? false;
425
+ try {
426
+ const fileSnapshot = await statSnapshot(filePath);
427
+ if (fileSnapshot === undefined && oldString === "") {
428
+ await mkdir(dirname(filePath), { recursive: true });
429
+ await writeFile(filePath, newString, "utf8");
430
+ const created = await statSnapshot(filePath);
431
+ if (created) {
432
+ fileCache.record(sessionID, filePath, created);
433
+ }
434
+ publishSuccessMetadata({
435
+ context,
436
+ filePath,
437
+ beforeContent: "",
438
+ afterContent: newString,
439
+ noopEdits: 0,
440
+ deduplicatedEdits: 0,
441
+ telemetry,
442
+ });
443
+ return `Created ${filePath}\n${formatReplaceSuccess(filePath, "", {
444
+ content: newString,
445
+ layer: "exact",
446
+ replacements: 1,
447
+ warnings: [],
448
+ })}`;
449
+ }
450
+ if (fileSnapshot === undefined) {
451
+ return `Error: File not found: ${filePath}`;
452
+ }
453
+ const verdict = fileCache.check(sessionID, filePath, fileSnapshot);
454
+ if (verdict === "unread") {
455
+ return `Error: ${filePath} has not been read in this session. Use the Read tool to load the file first, then retry the edit.`;
456
+ }
457
+ if (verdict === "drifted") {
458
+ return `Error: ${filePath} changed since it was last read. Re-read the file and retry the edit against the current content.`;
459
+ }
460
+ const rawContent = await readFile(filePath, "utf8");
461
+ const envelope = canonicalizeFileText(rawContent);
462
+ const result = applyReplaceEdit(envelope.content, { oldString, newString, replaceAll });
463
+ if (!result.ok) {
464
+ return `Error: ${result.error}`;
465
+ }
466
+ await writeFile(filePath, restoreFileText(result.content, envelope), "utf8");
467
+ const updated = await statSnapshot(filePath);
468
+ if (updated) {
469
+ fileCache.record(sessionID, filePath, updated);
470
+ }
471
+ publishSuccessMetadata({
472
+ context,
473
+ filePath,
474
+ beforeContent: envelope.content,
475
+ afterContent: result.content,
476
+ noopEdits: 0,
477
+ deduplicatedEdits: 0,
478
+ telemetry,
479
+ });
480
+ return formatReplaceSuccess(filePath, envelope.content, result);
481
+ }
482
+ catch (error) {
483
+ const message = error instanceof Error ? error.message : String(error);
484
+ return `Error: ${message}`;
485
+ }
486
+ }
487
+ // END_BLOCK_REPLACE_EXECUTE
488
+ // START_BLOCK_STR_REPLACE_EXECUTE
489
+ async function executeStrReplaceEditor(args, context, sessionID, fileCache, telemetry) {
490
+ const editor = new StrReplaceEditor({
491
+ fs: createNodeStrReplaceFs(),
492
+ onViewed: (path, snapshot) => fileCache.record(sessionID, path, snapshot),
493
+ checkFreshness: (path, current) => fileCache.check(sessionID, path, current),
494
+ });
495
+ const result = await editor.execute(args);
496
+ if (!result.ok) {
497
+ return `Error: ${result.error}`;
498
+ }
499
+ if (args.command !== "view") {
500
+ context.metadata({
501
+ title: args.path,
502
+ metadata: {
503
+ filePath: args.path,
504
+ path: args.path,
505
+ file: args.path,
506
+ editMode: telemetry.editMode,
507
+ providerID: telemetry.providerID,
508
+ modelID: telemetry.modelID,
509
+ },
510
+ });
511
+ }
512
+ return result.output;
513
+ }
514
+ // END_BLOCK_STR_REPLACE_EXECUTE
515
+ // START_BLOCK_PLUGIN
385
516
  export const HashlineEditPlugin = async ({ directory }) => {
386
517
  const vvoc = await loadVvocConfig({ cwd: directory });
387
- if (!isVvocPluginEnabled(vvoc.config, "hashline-edit"))
518
+ const settings = parseHashlineEditPluginEntry(vvoc.config.plugins?.["hashline-edit"]);
519
+ if (!settings.enabled)
388
520
  return {};
521
+ const routing = settings.routing;
522
+ const modelCache = new SessionModelCache();
523
+ const fileCache = new SessionFileCache();
524
+ const resolveSessionMode = (sessionID) => resolveEditMode(routing, modelCache.get(sessionID));
525
+ const telemetryFor = (sessionID) => {
526
+ const model = modelCache.get(sessionID);
527
+ return {
528
+ editMode: resolveEditMode(routing, model),
529
+ providerID: model?.providerID,
530
+ modelID: model?.modelID,
531
+ };
532
+ };
389
533
  return {
534
+ "chat.message": async (input, output) => {
535
+ const model = output.message?.model;
536
+ if (input.sessionID &&
537
+ model &&
538
+ typeof model.providerID === "string" &&
539
+ typeof model.modelID === "string") {
540
+ modelCache.set(input.sessionID, { providerID: model.providerID, modelID: model.modelID });
541
+ }
542
+ const mode = resolveEditMode(routing, model);
543
+ const visible = new Set(visibleToolsForMode(mode));
544
+ const message = output.message;
545
+ if (!message)
546
+ return;
547
+ const toolsMap = message.tools ?? {};
548
+ for (const toolName of EDIT_TYPE_TOOLS) {
549
+ if (!visible.has(toolName)) {
550
+ toolsMap[toolName] = false;
551
+ }
552
+ }
553
+ message.tools = toolsMap;
554
+ },
555
+ "tool.execute.before": async (input) => {
556
+ if (!isEditTypeTool(input.tool)) {
557
+ return;
558
+ }
559
+ const mode = resolveSessionMode(input.sessionID);
560
+ const visible = visibleToolsForMode(mode);
561
+ if (visible.includes(input.tool)) {
562
+ return;
563
+ }
564
+ const preferred = visible[0] ?? "the host-provided edit tool (for example apply_patch)";
565
+ throw new Error(`${input.tool} is not available for this session's model (edit mode: ${mode}). Use ${preferred} instead.`);
566
+ },
390
567
  "tool.execute.after": async (input, output) => {
391
- if (!isReadTool(input.tool) || typeof output.output !== "string") {
568
+ if (!isReadTool(input.tool)) {
569
+ return;
570
+ }
571
+ const filePath = readArgFilePath(input.args);
572
+ if (filePath) {
573
+ const snapshot = await statSnapshot(filePath);
574
+ if (snapshot) {
575
+ fileCache.record(input.sessionID, filePath, snapshot);
576
+ }
577
+ }
578
+ if (typeof output.output !== "string") {
579
+ return;
580
+ }
581
+ if (resolveSessionMode(input.sessionID) !== "hashline") {
392
582
  return;
393
583
  }
394
584
  if (!isHashlineEligibleReadOutput(output.output)) {
@@ -397,35 +587,84 @@ export const HashlineEditPlugin = async ({ directory }) => {
397
587
  output.output = transformReadOutput(output.output, await readSourceLines(input.args));
398
588
  },
399
589
  tool: {
400
- edit: tool({
590
+ hashline_edit: tool({
401
591
  description: HASHLINE_EDIT_DESCRIPTION,
402
592
  args: {
403
593
  filePath: z.string().describe("Absolute path to the file to edit"),
404
594
  delete: z.boolean().optional().describe("Delete the file instead of editing it"),
405
595
  rename: z.string().optional().describe("Rename the file after edits are applied"),
406
596
  edits: z
407
- .array(z
408
- .object({
597
+ .array(z.object({
409
598
  op: z.enum(["replace", "replace_range", "append", "prepend"]),
410
- pos: z.string().optional().describe("Primary anchor in LINE#HASH#ANCHOR format"),
599
+ pos: z
600
+ .string()
601
+ .optional()
602
+ .describe("Primary anchor in LINE#HASH#ANCHOR three-part format"),
411
603
  end: z
412
604
  .string()
413
605
  .optional()
414
- .describe("Range end for replace_range in LINE#HASH#ANCHOR format (required for replace_range, rejected for replace)"),
606
+ .describe("Optional range end anchor in LINE#HASH#ANCHOR format. With end, replace covers the inclusive range pos..end; required when op is replace_range."),
415
607
  lines: z
416
608
  .union([z.array(z.string()), z.string(), z.null()])
417
609
  .describe("Replacement or inserted lines as plain text content"),
418
- })
419
- .refine((obj) => !(obj.op === "replace" && Array.isArray(obj.lines) && obj.lines.length > 1), {
420
- message: "replace accepts at most 1 replacement line. " +
421
- "Use array with 1 element, a string, or null. " +
422
- "For multi-line replacement, use replace_range with pos + end.",
423
610
  }))
424
611
  .describe("Hash-anchored edit operations to apply to the file"),
425
612
  },
426
- execute: (args, context) => executeHashlineEdit(args, context),
613
+ execute: (args, context) => executeHashlineEdit(args, context, telemetryFor(context.sessionID)),
614
+ }),
615
+ edit: tool({
616
+ description: REPLACE_EDIT_DESCRIPTION,
617
+ args: {
618
+ filePath: z.string().optional().describe("Absolute path to the file to modify"),
619
+ file_path: z
620
+ .string()
621
+ .optional()
622
+ .describe("Alias of filePath (snake_case form used by some models)"),
623
+ oldString: z.string().optional().describe("Exact text to replace"),
624
+ old_string: z.string().optional().describe("Alias of oldString"),
625
+ newString: z.string().optional().describe("Replacement text"),
626
+ new_string: z.string().optional().describe("Alias of newString"),
627
+ replaceAll: z
628
+ .boolean()
629
+ .optional()
630
+ .describe("Replace every occurrence instead of requiring a unique match"),
631
+ replace_all: z.boolean().optional().describe("Alias of replaceAll"),
632
+ },
633
+ execute: (args, context) => executeReplaceEdit(args, context, context.sessionID, fileCache, telemetryFor(context.sessionID)),
634
+ }),
635
+ str_replace_editor: tool({
636
+ description: STR_REPLACE_EDITOR_DESCRIPTION,
637
+ args: {
638
+ command: z
639
+ .enum(["view", "create", "str_replace", "insert"])
640
+ .describe("The command to run: view, create, str_replace, or insert"),
641
+ path: z.string().describe("Absolute path to file or directory"),
642
+ file_text: z
643
+ .string()
644
+ .optional()
645
+ .describe("Required parameter of create command with the content of the new file"),
646
+ old_str: z
647
+ .string()
648
+ .optional()
649
+ .describe("Required parameter of str_replace command: the exact text to replace"),
650
+ new_str: z
651
+ .string()
652
+ .optional()
653
+ .describe("Optional parameter of str_replace command with the replacement text; required for insert"),
654
+ insert_line: z
655
+ .number()
656
+ .int()
657
+ .optional()
658
+ .describe("Required parameter of insert command; new_str is inserted AFTER this line"),
659
+ view_range: z
660
+ .array(z.number().int())
661
+ .optional()
662
+ .describe("Optional [start, end] line range for view; end may be -1 for end of file"),
663
+ },
664
+ execute: (args, context) => executeStrReplaceEditor(args, context, context.sessionID, fileCache, telemetryFor(context.sessionID)),
427
665
  }),
428
666
  },
429
667
  };
430
668
  };
669
+ // END_BLOCK_PLUGIN
431
670
  //# sourceMappingURL=index.js.map