opencode-hashline 1.0.2 → 1.0.4

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.
@@ -111,13 +111,14 @@ function stripHashes(content, prefix) {
111
111
  const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
112
  let hashLinePattern = stripRegexCache.get(escapedPrefix);
113
113
  if (!hashLinePattern) {
114
- hashLinePattern = new RegExp(`^${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
114
+ hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
115
115
  stripRegexCache.set(escapedPrefix, hashLinePattern);
116
116
  }
117
117
  return content.split("\n").map((line) => {
118
118
  const match = line.match(hashLinePattern);
119
119
  if (match) {
120
- return line.slice(match[0].length);
120
+ const patchMarker = match[1] || "";
121
+ return patchMarker + line.slice(match[0].length);
121
122
  }
122
123
  return line;
123
124
  }).join("\n");
@@ -4,13 +4,26 @@ import {
4
4
  resolveConfig,
5
5
  shouldExclude,
6
6
  stripHashes
7
- } from "./chunk-IVZSANZ4.js";
7
+ } from "./chunk-3AJXTHJ3.js";
8
8
 
9
9
  // src/hooks.ts
10
10
  import { appendFileSync } from "fs";
11
11
  import { join } from "path";
12
12
  import { homedir } from "os";
13
13
  var DEBUG_LOG = join(homedir(), ".config", "opencode", "hashline-debug.log");
14
+ var MAX_PROCESSED_IDS = 1e4;
15
+ function createBoundedSet(maxSize) {
16
+ const set = /* @__PURE__ */ new Set();
17
+ const originalAdd = set.add.bind(set);
18
+ set.add = (value) => {
19
+ if (set.size >= maxSize) {
20
+ const first = set.values().next().value;
21
+ if (first !== void 0) set.delete(first);
22
+ }
23
+ return originalAdd(value);
24
+ };
25
+ return set;
26
+ }
14
27
  function debug(...args) {
15
28
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
16
29
  `;
@@ -40,7 +53,7 @@ function createFileReadAfterHook(cache, config) {
40
53
  const resolved = config ?? resolveConfig();
41
54
  const hashLen = resolved.hashLength || 0;
42
55
  const prefix = resolved.prefix;
43
- const processedCallIds = /* @__PURE__ */ new Set();
56
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
44
57
  return async (input, output) => {
45
58
  debug("tool.execute.after:", input.tool, "args:", input.args);
46
59
  if (input.callID) {
@@ -87,7 +100,7 @@ function createFileReadAfterHook(cache, config) {
87
100
  function createFileEditBeforeHook(config) {
88
101
  const resolved = config ?? resolveConfig();
89
102
  const prefix = resolved.prefix;
90
- const processedCallIds = /* @__PURE__ */ new Set();
103
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
91
104
  return async (input, output) => {
92
105
  if (input.callID) {
93
106
  if (processedCallIds.has(input.callID)) {
@@ -101,7 +114,7 @@ function createFileEditBeforeHook(config) {
101
114
  );
102
115
  if (!isFileEdit) return;
103
116
  if (!output.args || typeof output.args !== "object") return;
104
- const contentFields = [
117
+ const contentFields = /* @__PURE__ */ new Set([
105
118
  "content",
106
119
  "new_content",
107
120
  "old_content",
@@ -111,13 +124,26 @@ function createFileEditBeforeHook(config) {
111
124
  "text",
112
125
  "diff",
113
126
  "patch",
114
- "patchText"
115
- ];
116
- for (const field of contentFields) {
117
- if (typeof output.args[field] === "string") {
118
- output.args[field] = stripHashes(output.args[field], prefix);
127
+ "patchText",
128
+ "body"
129
+ ]);
130
+ function stripDeep(obj) {
131
+ for (const key of Object.keys(obj)) {
132
+ const val = obj[key];
133
+ if (typeof val === "string" && contentFields.has(key)) {
134
+ obj[key] = stripHashes(val, prefix);
135
+ } else if (Array.isArray(val)) {
136
+ for (const item of val) {
137
+ if (item && typeof item === "object" && !Array.isArray(item)) {
138
+ stripDeep(item);
139
+ }
140
+ }
141
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
142
+ stripDeep(val);
143
+ }
119
144
  }
120
145
  }
146
+ stripDeep(output.args);
121
147
  };
122
148
  }
123
149
  function createSystemPromptHook(config) {
@@ -19,7 +19,7 @@ import {
19
19
  shouldExclude,
20
20
  stripHashes,
21
21
  verifyHash
22
- } from "./chunk-IVZSANZ4.js";
22
+ } from "./chunk-3AJXTHJ3.js";
23
23
  export {
24
24
  DEFAULT_CONFIG,
25
25
  DEFAULT_EXCLUDE_PATTERNS,
package/dist/index.cjs CHANGED
@@ -123,13 +123,14 @@ function stripHashes(content, prefix) {
123
123
  const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
124
124
  let hashLinePattern = stripRegexCache.get(escapedPrefix);
125
125
  if (!hashLinePattern) {
126
- hashLinePattern = new RegExp(`^${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
126
+ hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
127
127
  stripRegexCache.set(escapedPrefix, hashLinePattern);
128
128
  }
129
129
  return content.split("\n").map((line) => {
130
130
  const match = line.match(hashLinePattern);
131
131
  if (match) {
132
- return line.slice(match[0].length);
132
+ const patchMarker = match[1] || "";
133
+ return patchMarker + line.slice(match[0].length);
133
134
  }
134
135
  return line;
135
136
  }).join("\n");
@@ -467,6 +468,19 @@ var import_path = require("path");
467
468
  var import_os = require("os");
468
469
  init_hashline();
469
470
  var DEBUG_LOG = (0, import_path.join)((0, import_os.homedir)(), ".config", "opencode", "hashline-debug.log");
471
+ var MAX_PROCESSED_IDS = 1e4;
472
+ function createBoundedSet(maxSize) {
473
+ const set = /* @__PURE__ */ new Set();
474
+ const originalAdd = set.add.bind(set);
475
+ set.add = (value) => {
476
+ if (set.size >= maxSize) {
477
+ const first = set.values().next().value;
478
+ if (first !== void 0) set.delete(first);
479
+ }
480
+ return originalAdd(value);
481
+ };
482
+ return set;
483
+ }
470
484
  function debug(...args) {
471
485
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
472
486
  `;
@@ -496,7 +510,7 @@ function createFileReadAfterHook(cache, config) {
496
510
  const resolved = config ?? resolveConfig();
497
511
  const hashLen = resolved.hashLength || 0;
498
512
  const prefix = resolved.prefix;
499
- const processedCallIds = /* @__PURE__ */ new Set();
513
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
500
514
  return async (input, output) => {
501
515
  debug("tool.execute.after:", input.tool, "args:", input.args);
502
516
  if (input.callID) {
@@ -543,7 +557,7 @@ function createFileReadAfterHook(cache, config) {
543
557
  function createFileEditBeforeHook(config) {
544
558
  const resolved = config ?? resolveConfig();
545
559
  const prefix = resolved.prefix;
546
- const processedCallIds = /* @__PURE__ */ new Set();
560
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
547
561
  return async (input, output) => {
548
562
  if (input.callID) {
549
563
  if (processedCallIds.has(input.callID)) {
@@ -557,7 +571,7 @@ function createFileEditBeforeHook(config) {
557
571
  );
558
572
  if (!isFileEdit) return;
559
573
  if (!output.args || typeof output.args !== "object") return;
560
- const contentFields = [
574
+ const contentFields = /* @__PURE__ */ new Set([
561
575
  "content",
562
576
  "new_content",
563
577
  "old_content",
@@ -567,13 +581,26 @@ function createFileEditBeforeHook(config) {
567
581
  "text",
568
582
  "diff",
569
583
  "patch",
570
- "patchText"
571
- ];
572
- for (const field of contentFields) {
573
- if (typeof output.args[field] === "string") {
574
- output.args[field] = stripHashes(output.args[field], prefix);
584
+ "patchText",
585
+ "body"
586
+ ]);
587
+ function stripDeep(obj) {
588
+ for (const key of Object.keys(obj)) {
589
+ const val = obj[key];
590
+ if (typeof val === "string" && contentFields.has(key)) {
591
+ obj[key] = stripHashes(val, prefix);
592
+ } else if (Array.isArray(val)) {
593
+ for (const item of val) {
594
+ if (item && typeof item === "object" && !Array.isArray(item)) {
595
+ stripDeep(item);
596
+ }
597
+ }
598
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
599
+ stripDeep(val);
600
+ }
575
601
  }
576
602
  }
603
+ stripDeep(output.args);
577
604
  };
578
605
  }
579
606
  function createSystemPromptHook(config) {
@@ -661,15 +688,21 @@ function createHashlineEditTool(config, cache) {
661
688
  async execute(args, context) {
662
689
  const { path, operation, startRef, endRef, replacement } = args;
663
690
  const absPath = (0, import_path2.isAbsolute)(path) ? path : (0, import_path2.resolve)(context.directory, path);
664
- const normalizedAbs = (0, import_path2.resolve)(absPath);
665
- const normalizedWorktree = (0, import_path2.resolve)(context.worktree);
666
- if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + import_path2.sep)) {
691
+ let realAbs;
692
+ try {
693
+ realAbs = (0, import_fs2.realpathSync)(absPath);
694
+ } catch {
695
+ realAbs = (0, import_path2.resolve)(absPath);
696
+ }
697
+ const realWorktree = (0, import_fs2.realpathSync)((0, import_path2.resolve)(context.worktree));
698
+ if (realAbs !== realWorktree && !realAbs.startsWith(realWorktree + import_path2.sep)) {
667
699
  throw new Error(`Access denied: "${path}" resolves outside the project directory`);
668
700
  }
701
+ const normalizedAbs = (0, import_path2.resolve)(absPath);
669
702
  const displayPath = (0, import_path2.relative)(context.worktree, absPath) || path;
670
703
  let current;
671
704
  try {
672
- current = (0, import_fs2.readFileSync)(absPath, "utf-8");
705
+ current = (0, import_fs2.readFileSync)(realAbs, "utf-8");
673
706
  } catch (error) {
674
707
  const reason = error instanceof Error ? error.message : String(error);
675
708
  throw new Error(`Failed to read "${displayPath}": ${reason}`);
@@ -696,14 +729,15 @@ function createHashlineEditTool(config, cache) {
696
729
  throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
697
730
  }
698
731
  try {
699
- (0, import_fs2.writeFileSync)(absPath, nextContent, "utf-8");
732
+ (0, import_fs2.writeFileSync)(realAbs, nextContent, "utf-8");
700
733
  } catch (error) {
701
734
  const reason = error instanceof Error ? error.message : String(error);
702
735
  throw new Error(`Failed to write "${displayPath}": ${reason}`);
703
736
  }
704
737
  if (cache) {
705
- cache.invalidate(absPath);
738
+ cache.invalidate(realAbs);
706
739
  cache.invalidate(normalizedAbs);
740
+ cache.invalidate(absPath);
707
741
  if (path !== absPath) cache.invalidate(path);
708
742
  if (displayPath !== absPath) cache.invalidate(displayPath);
709
743
  }
@@ -751,6 +785,7 @@ function loadConfig(projectDir, userConfig) {
751
785
  function createHashlinePlugin(userConfig) {
752
786
  return async (input) => {
753
787
  const projectDir = input.directory;
788
+ const worktree = input.worktree;
754
789
  const fileConfig = loadConfig(projectDir, userConfig);
755
790
  const config = resolveConfig(fileConfig);
756
791
  const cache = new HashlineCache(config.cacheSize);
@@ -761,6 +796,17 @@ function createHashlinePlugin(userConfig) {
761
796
  `);
762
797
  } catch {
763
798
  }
799
+ const tempFiles = /* @__PURE__ */ new Set();
800
+ const cleanupTempFiles = () => {
801
+ for (const f of tempFiles) {
802
+ try {
803
+ (0, import_fs3.unlinkSync)(f);
804
+ } catch {
805
+ }
806
+ }
807
+ tempFiles.clear();
808
+ };
809
+ process.on("exit", cleanupTempFiles);
764
810
  return {
765
811
  tool: {
766
812
  hashline_edit: createHashlineEditTool(config, cache)
@@ -782,6 +828,17 @@ function createHashlinePlugin(userConfig) {
782
828
  filePath = (0, import_url.fileURLToPath)(p.url);
783
829
  }
784
830
  if (!filePath) continue;
831
+ if (worktree) {
832
+ try {
833
+ const realFile = (0, import_fs3.realpathSync)(filePath);
834
+ const realWorktree = (0, import_fs3.realpathSync)((0, import_path3.resolve)(worktree));
835
+ if (realFile !== realWorktree && !realFile.startsWith(realWorktree + import_path3.sep)) {
836
+ continue;
837
+ }
838
+ } catch {
839
+ continue;
840
+ }
841
+ }
785
842
  if (shouldExclude2(filePath, config.exclude)) continue;
786
843
  let content;
787
844
  try {
@@ -794,6 +851,7 @@ function createHashlinePlugin(userConfig) {
794
851
  if (cached) {
795
852
  const tmpPath2 = (0, import_path3.join)((0, import_os2.tmpdir)(), `hashline-${p.id}.txt`);
796
853
  (0, import_fs3.writeFileSync)(tmpPath2, cached, "utf-8");
854
+ tempFiles.add(tmpPath2);
797
855
  p.url = `file://${tmpPath2}`;
798
856
  writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated (cached): ${filePath}
799
857
  `);
package/dist/index.js CHANGED
@@ -2,21 +2,21 @@ import {
2
2
  createFileEditBeforeHook,
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook
5
- } from "./chunk-VW5NAHEY.js";
5
+ } from "./chunk-DUTTCCXT.js";
6
6
  import {
7
7
  HashlineCache,
8
8
  applyHashEdit,
9
9
  resolveConfig
10
- } from "./chunk-IVZSANZ4.js";
10
+ } from "./chunk-3AJXTHJ3.js";
11
11
 
12
12
  // src/index.ts
13
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
14
- import { join } from "path";
13
+ import { readFileSync as readFileSync2, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
14
+ import { join, resolve as resolve2, sep as sep2 } from "path";
15
15
  import { homedir, tmpdir } from "os";
16
16
  import { fileURLToPath } from "url";
17
17
 
18
18
  // src/hashline-tool.ts
19
- import { readFileSync, writeFileSync } from "fs";
19
+ import { readFileSync, realpathSync, writeFileSync } from "fs";
20
20
  import { isAbsolute, relative, resolve, sep } from "path";
21
21
  import { z } from "zod";
22
22
  function createHashlineEditTool(config, cache) {
@@ -32,15 +32,21 @@ function createHashlineEditTool(config, cache) {
32
32
  async execute(args, context) {
33
33
  const { path, operation, startRef, endRef, replacement } = args;
34
34
  const absPath = isAbsolute(path) ? path : resolve(context.directory, path);
35
- const normalizedAbs = resolve(absPath);
36
- const normalizedWorktree = resolve(context.worktree);
37
- if (normalizedAbs !== normalizedWorktree && !normalizedAbs.startsWith(normalizedWorktree + sep)) {
35
+ let realAbs;
36
+ try {
37
+ realAbs = realpathSync(absPath);
38
+ } catch {
39
+ realAbs = resolve(absPath);
40
+ }
41
+ const realWorktree = realpathSync(resolve(context.worktree));
42
+ if (realAbs !== realWorktree && !realAbs.startsWith(realWorktree + sep)) {
38
43
  throw new Error(`Access denied: "${path}" resolves outside the project directory`);
39
44
  }
45
+ const normalizedAbs = resolve(absPath);
40
46
  const displayPath = relative(context.worktree, absPath) || path;
41
47
  let current;
42
48
  try {
43
- current = readFileSync(absPath, "utf-8");
49
+ current = readFileSync(realAbs, "utf-8");
44
50
  } catch (error) {
45
51
  const reason = error instanceof Error ? error.message : String(error);
46
52
  throw new Error(`Failed to read "${displayPath}": ${reason}`);
@@ -67,14 +73,15 @@ function createHashlineEditTool(config, cache) {
67
73
  throw new Error(`Hashline edit failed for "${displayPath}": ${reason}`);
68
74
  }
69
75
  try {
70
- writeFileSync(absPath, nextContent, "utf-8");
76
+ writeFileSync(realAbs, nextContent, "utf-8");
71
77
  } catch (error) {
72
78
  const reason = error instanceof Error ? error.message : String(error);
73
79
  throw new Error(`Failed to write "${displayPath}": ${reason}`);
74
80
  }
75
81
  if (cache) {
76
- cache.invalidate(absPath);
82
+ cache.invalidate(realAbs);
77
83
  cache.invalidate(normalizedAbs);
84
+ cache.invalidate(absPath);
78
85
  if (path !== absPath) cache.invalidate(path);
79
86
  if (displayPath !== absPath) cache.invalidate(displayPath);
80
87
  }
@@ -122,6 +129,7 @@ function loadConfig(projectDir, userConfig) {
122
129
  function createHashlinePlugin(userConfig) {
123
130
  return async (input) => {
124
131
  const projectDir = input.directory;
132
+ const worktree = input.worktree;
125
133
  const fileConfig = loadConfig(projectDir, userConfig);
126
134
  const config = resolveConfig(fileConfig);
127
135
  const cache = new HashlineCache(config.cacheSize);
@@ -132,6 +140,17 @@ function createHashlinePlugin(userConfig) {
132
140
  `);
133
141
  } catch {
134
142
  }
143
+ const tempFiles = /* @__PURE__ */ new Set();
144
+ const cleanupTempFiles = () => {
145
+ for (const f of tempFiles) {
146
+ try {
147
+ unlinkSync(f);
148
+ } catch {
149
+ }
150
+ }
151
+ tempFiles.clear();
152
+ };
153
+ process.on("exit", cleanupTempFiles);
135
154
  return {
136
155
  tool: {
137
156
  hashline_edit: createHashlineEditTool(config, cache)
@@ -144,7 +163,7 @@ function createHashlinePlugin(userConfig) {
144
163
  const out = output;
145
164
  const hashLen = config.hashLength || 0;
146
165
  const prefix = config.prefix;
147
- const { formatFileWithHashes, shouldExclude, getByteLength } = await import("./hashline-W2FT5QN4.js");
166
+ const { formatFileWithHashes, shouldExclude, getByteLength } = await import("./hashline-V3FR43UZ.js");
148
167
  for (const p of out.parts ?? []) {
149
168
  if (p.type !== "file") continue;
150
169
  if (!p.url || !p.mime?.startsWith("text/")) continue;
@@ -153,6 +172,17 @@ function createHashlinePlugin(userConfig) {
153
172
  filePath = fileURLToPath(p.url);
154
173
  }
155
174
  if (!filePath) continue;
175
+ if (worktree) {
176
+ try {
177
+ const realFile = realpathSync2(filePath);
178
+ const realWorktree = realpathSync2(resolve2(worktree));
179
+ if (realFile !== realWorktree && !realFile.startsWith(realWorktree + sep2)) {
180
+ continue;
181
+ }
182
+ } catch {
183
+ continue;
184
+ }
185
+ }
156
186
  if (shouldExclude(filePath, config.exclude)) continue;
157
187
  let content;
158
188
  try {
@@ -165,6 +195,7 @@ function createHashlinePlugin(userConfig) {
165
195
  if (cached) {
166
196
  const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
167
197
  writeFileSync2(tmpPath2, cached, "utf-8");
198
+ tempFiles.add(tmpPath2);
168
199
  p.url = `file://${tmpPath2}`;
169
200
  writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] chat.message annotated (cached): ${filePath}
170
201
  `);
package/dist/utils.cjs CHANGED
@@ -170,13 +170,14 @@ function stripHashes(content, prefix) {
170
170
  const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
171
171
  let hashLinePattern = stripRegexCache.get(escapedPrefix);
172
172
  if (!hashLinePattern) {
173
- hashLinePattern = new RegExp(`^${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
173
+ hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
174
174
  stripRegexCache.set(escapedPrefix, hashLinePattern);
175
175
  }
176
176
  return content.split("\n").map((line) => {
177
177
  const match = line.match(hashLinePattern);
178
178
  if (match) {
179
- return line.slice(match[0].length);
179
+ const patchMarker = match[1] || "";
180
+ return patchMarker + line.slice(match[0].length);
180
181
  }
181
182
  return line;
182
183
  }).join("\n");
@@ -451,6 +452,19 @@ var import_fs = require("fs");
451
452
  var import_path = require("path");
452
453
  var import_os = require("os");
453
454
  var DEBUG_LOG = (0, import_path.join)((0, import_os.homedir)(), ".config", "opencode", "hashline-debug.log");
455
+ var MAX_PROCESSED_IDS = 1e4;
456
+ function createBoundedSet(maxSize) {
457
+ const set = /* @__PURE__ */ new Set();
458
+ const originalAdd = set.add.bind(set);
459
+ set.add = (value) => {
460
+ if (set.size >= maxSize) {
461
+ const first = set.values().next().value;
462
+ if (first !== void 0) set.delete(first);
463
+ }
464
+ return originalAdd(value);
465
+ };
466
+ return set;
467
+ }
454
468
  function debug(...args) {
455
469
  const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
456
470
  `;
@@ -480,7 +494,7 @@ function createFileReadAfterHook(cache, config) {
480
494
  const resolved = config ?? resolveConfig();
481
495
  const hashLen = resolved.hashLength || 0;
482
496
  const prefix = resolved.prefix;
483
- const processedCallIds = /* @__PURE__ */ new Set();
497
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
484
498
  return async (input, output) => {
485
499
  debug("tool.execute.after:", input.tool, "args:", input.args);
486
500
  if (input.callID) {
@@ -527,7 +541,7 @@ function createFileReadAfterHook(cache, config) {
527
541
  function createFileEditBeforeHook(config) {
528
542
  const resolved = config ?? resolveConfig();
529
543
  const prefix = resolved.prefix;
530
- const processedCallIds = /* @__PURE__ */ new Set();
544
+ const processedCallIds = createBoundedSet(MAX_PROCESSED_IDS);
531
545
  return async (input, output) => {
532
546
  if (input.callID) {
533
547
  if (processedCallIds.has(input.callID)) {
@@ -541,7 +555,7 @@ function createFileEditBeforeHook(config) {
541
555
  );
542
556
  if (!isFileEdit) return;
543
557
  if (!output.args || typeof output.args !== "object") return;
544
- const contentFields = [
558
+ const contentFields = /* @__PURE__ */ new Set([
545
559
  "content",
546
560
  "new_content",
547
561
  "old_content",
@@ -551,13 +565,26 @@ function createFileEditBeforeHook(config) {
551
565
  "text",
552
566
  "diff",
553
567
  "patch",
554
- "patchText"
555
- ];
556
- for (const field of contentFields) {
557
- if (typeof output.args[field] === "string") {
558
- output.args[field] = stripHashes(output.args[field], prefix);
568
+ "patchText",
569
+ "body"
570
+ ]);
571
+ function stripDeep(obj) {
572
+ for (const key of Object.keys(obj)) {
573
+ const val = obj[key];
574
+ if (typeof val === "string" && contentFields.has(key)) {
575
+ obj[key] = stripHashes(val, prefix);
576
+ } else if (Array.isArray(val)) {
577
+ for (const item of val) {
578
+ if (item && typeof item === "object" && !Array.isArray(item)) {
579
+ stripDeep(item);
580
+ }
581
+ }
582
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
583
+ stripDeep(val);
584
+ }
559
585
  }
560
586
  }
587
+ stripDeep(output.args);
561
588
  };
562
589
  }
563
590
  function createSystemPromptHook(config) {
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  isFileReadTool
6
- } from "./chunk-VW5NAHEY.js";
6
+ } from "./chunk-DUTTCCXT.js";
7
7
  import {
8
8
  DEFAULT_CONFIG,
9
9
  DEFAULT_EXCLUDE_PATTERNS,
@@ -25,7 +25,7 @@ import {
25
25
  shouldExclude,
26
26
  stripHashes,
27
27
  verifyHash
28
- } from "./chunk-IVZSANZ4.js";
28
+ } from "./chunk-3AJXTHJ3.js";
29
29
  export {
30
30
  DEFAULT_CONFIG,
31
31
  DEFAULT_EXCLUDE_PATTERNS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hashline",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Hashline plugin for OpenCode — content-addressable line hashing for precise AI code editing",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",