opencode-hashline 1.1.1 → 1.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.
@@ -28,7 +28,24 @@ var DEFAULT_EXCLUDE_PATTERNS = [
28
28
  "**/*.exe",
29
29
  "**/*.dll",
30
30
  "**/*.so",
31
- "**/*.dylib"
31
+ "**/*.dylib",
32
+ // Sensitive credential and secret files
33
+ "**/.env",
34
+ "**/.env.*",
35
+ "**/*.pem",
36
+ "**/*.key",
37
+ "**/*.p12",
38
+ "**/*.pfx",
39
+ "**/id_rsa",
40
+ "**/id_rsa.pub",
41
+ "**/id_ed25519",
42
+ "**/id_ed25519.pub",
43
+ "**/id_ecdsa",
44
+ "**/id_ecdsa.pub",
45
+ "**/.npmrc",
46
+ "**/.netrc",
47
+ "**/credentials",
48
+ "**/credentials.json"
32
49
  ];
33
50
  var DEFAULT_PREFIX = "#HL ";
34
51
  var DEFAULT_CONFIG = {
@@ -88,16 +105,24 @@ function computeLineHash(idx, line, hashLen = 3) {
88
105
  return hash.toString(16).padStart(hashLen, "0");
89
106
  }
90
107
  function formatFileWithHashes(content, hashLen, prefix) {
91
- const lines = content.split("\n");
108
+ const normalized = content.includes("\r\n") ? content.replace(/\r\n/g, "\n") : content;
109
+ const lines = normalized.split("\n");
92
110
  const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
93
111
  const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
94
112
  const hashes = new Array(lines.length);
95
113
  const seen = /* @__PURE__ */ new Map();
114
+ const upgraded = /* @__PURE__ */ new Set();
96
115
  for (let idx = 0; idx < lines.length; idx++) {
97
116
  const hash = computeLineHash(idx, lines[idx], effectiveLen);
98
117
  if (seen.has(hash)) {
99
118
  const longerLen = Math.min(effectiveLen + 1, 8);
119
+ const prevIdx = seen.get(hash);
120
+ if (!upgraded.has(prevIdx)) {
121
+ hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
122
+ upgraded.add(prevIdx);
123
+ }
100
124
  hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
125
+ upgraded.add(idx);
101
126
  } else {
102
127
  seen.set(hash, idx);
103
128
  hashes[idx] = hash;
@@ -116,7 +141,9 @@ function stripHashes(content, prefix) {
116
141
  hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
117
142
  stripRegexCache.set(escapedPrefix, hashLinePattern);
118
143
  }
119
- return content.split("\n").map((line) => {
144
+ const lineEnding = detectLineEnding(content);
145
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
146
+ const result = normalized.split("\n").map((line) => {
120
147
  const match = line.match(hashLinePattern);
121
148
  if (match) {
122
149
  const patchMarker = match[1] || "";
@@ -124,11 +151,13 @@ function stripHashes(content, prefix) {
124
151
  }
125
152
  return line;
126
153
  }).join("\n");
154
+ return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
127
155
  }
128
156
  function parseHashRef(ref) {
129
157
  const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
130
158
  if (!match) {
131
- throw new Error(`Invalid hash reference: "${ref}". Expected format: "<line>:<2-8 char hex>"`);
159
+ const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
160
+ throw new Error(`Invalid hash reference: "${display}". Expected format: "<line>:<2-8 char hex>"`);
132
161
  }
133
162
  return {
134
163
  line: parseInt(match[1], 10),
@@ -145,8 +174,9 @@ function normalizeHashRef(ref) {
145
174
  if (annotated) {
146
175
  return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
147
176
  }
177
+ const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
148
178
  throw new Error(
149
- `Invalid hash reference: "${ref}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
179
+ `Invalid hash reference: "${display}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
150
180
  );
151
181
  }
152
182
  function buildHashMap(content, hashLen) {
@@ -189,12 +219,14 @@ function resolveRange(startRef, endRef, content, hashLen) {
189
219
  `Invalid range: start line ${start.line} is after end line ${end.line}`
190
220
  );
191
221
  }
192
- const lines = content.split("\n");
193
- const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
222
+ const lineEnding = detectLineEnding(content);
223
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
224
+ const lines = normalized.split("\n");
225
+ const startVerify = verifyHash(start.line, start.hash, normalized, hashLen, lines);
194
226
  if (!startVerify.valid) {
195
227
  throw new Error(`Start reference invalid: ${startVerify.message}`);
196
228
  }
197
- const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
229
+ const endVerify = verifyHash(end.line, end.hash, normalized, hashLen, lines);
198
230
  if (!endVerify.valid) {
199
231
  throw new Error(`End reference invalid: ${endVerify.message}`);
200
232
  }
@@ -203,22 +235,27 @@ function resolveRange(startRef, endRef, content, hashLen) {
203
235
  startLine: start.line,
204
236
  endLine: end.line,
205
237
  lines: rangeLines,
206
- content: rangeLines.join("\n")
238
+ content: rangeLines.join(lineEnding)
207
239
  };
208
240
  }
209
241
  function replaceRange(startRef, endRef, content, replacement, hashLen) {
210
- const range = resolveRange(startRef, endRef, content, hashLen);
211
- const lines = content.split("\n");
242
+ const lineEnding = detectLineEnding(content);
243
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
244
+ const range = resolveRange(startRef, endRef, normalized, hashLen);
245
+ const lines = normalized.split("\n");
212
246
  const before = lines.slice(0, range.startLine - 1);
213
247
  const after = lines.slice(range.endLine);
214
248
  const replacementLines = replacement.split("\n");
215
- return [...before, ...replacementLines, ...after].join("\n");
249
+ const result = [...before, ...replacementLines, ...after].join("\n");
250
+ return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
216
251
  }
217
252
  function applyHashEdit(input, content, hashLen) {
253
+ const lineEnding = detectLineEnding(content);
254
+ const workContent = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
218
255
  const normalizedStart = normalizeHashRef(input.startRef);
219
256
  const start = parseHashRef(normalizedStart);
220
- const lines = content.split("\n");
221
- const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
257
+ const lines = workContent.split("\n");
258
+ const startVerify = verifyHash(start.line, start.hash, workContent, hashLen, lines);
222
259
  if (!startVerify.valid) {
223
260
  throw new Error(`Start reference invalid: ${startVerify.message}`);
224
261
  }
@@ -237,7 +274,7 @@ function applyHashEdit(input, content, hashLen) {
237
274
  operation: input.operation,
238
275
  startLine: start.line,
239
276
  endLine: start.line,
240
- content: next2
277
+ content: lineEnding === "\r\n" ? next2.replace(/\n/g, "\r\n") : next2
241
278
  };
242
279
  }
243
280
  const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
@@ -247,7 +284,7 @@ function applyHashEdit(input, content, hashLen) {
247
284
  `Invalid range: start line ${start.line} is after end line ${end.line}`
248
285
  );
249
286
  }
250
- const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
287
+ const endVerify = verifyHash(end.line, end.hash, workContent, hashLen, lines);
251
288
  if (!endVerify.valid) {
252
289
  throw new Error(`End reference invalid: ${endVerify.message}`);
253
290
  }
@@ -263,7 +300,7 @@ function applyHashEdit(input, content, hashLen) {
263
300
  operation: input.operation,
264
301
  startLine: start.line,
265
302
  endLine: end.line,
266
- content: next
303
+ content: lineEnding === "\r\n" ? next.replace(/\n/g, "\r\n") : next
267
304
  };
268
305
  }
269
306
  var HashlineCache = class {
@@ -325,10 +362,15 @@ var HashlineCache = class {
325
362
  return this.cache.size;
326
363
  }
327
364
  };
365
+ var globMatcherCache = /* @__PURE__ */ new Map();
328
366
  function matchesGlob(filePath, pattern) {
329
367
  const normalizedPath = filePath.replace(/\\/g, "/");
330
368
  const normalizedPattern = pattern.replace(/\\/g, "/");
331
- const isMatch = picomatch(normalizedPattern, { dot: true });
369
+ let isMatch = globMatcherCache.get(normalizedPattern);
370
+ if (!isMatch) {
371
+ isMatch = picomatch(normalizedPattern, { dot: true });
372
+ globMatcherCache.set(normalizedPattern, isMatch);
373
+ }
332
374
  return isMatch(normalizedPath);
333
375
  }
334
376
  function shouldExclude(filePath, patterns) {
@@ -338,6 +380,9 @@ var textEncoder = new TextEncoder();
338
380
  function getByteLength(content) {
339
381
  return textEncoder.encode(content).length;
340
382
  }
383
+ function detectLineEnding(content) {
384
+ return content.includes("\r\n") ? "\r\n" : "\n";
385
+ }
341
386
  function createHashline(config) {
342
387
  const resolved = resolveConfig(config);
343
388
  const cache = new HashlineCache(resolved.cacheSize);
@@ -410,5 +455,6 @@ export {
410
455
  matchesGlob,
411
456
  shouldExclude,
412
457
  getByteLength,
458
+ detectLineEnding,
413
459
  createHashline
414
460
  };
@@ -4,7 +4,7 @@ import {
4
4
  resolveConfig,
5
5
  shouldExclude,
6
6
  stripHashes
7
- } from "./chunk-DDXOFWTU.js";
7
+ } from "./chunk-I6RACR3D.js";
8
8
 
9
9
  // src/hooks.ts
10
10
  import { appendFileSync } from "fs";
@@ -7,6 +7,7 @@ import {
7
7
  buildHashMap,
8
8
  computeLineHash,
9
9
  createHashline,
10
+ detectLineEnding,
10
11
  formatFileWithHashes,
11
12
  getAdaptiveHashLength,
12
13
  getByteLength,
@@ -19,7 +20,7 @@ import {
19
20
  shouldExclude,
20
21
  stripHashes,
21
22
  verifyHash
22
- } from "./chunk-DDXOFWTU.js";
23
+ } from "./chunk-I6RACR3D.js";
23
24
  export {
24
25
  DEFAULT_CONFIG,
25
26
  DEFAULT_EXCLUDE_PATTERNS,
@@ -29,6 +30,7 @@ export {
29
30
  buildHashMap,
30
31
  computeLineHash,
31
32
  createHashline,
33
+ detectLineEnding,
32
34
  formatFileWithHashes,
33
35
  getAdaptiveHashLength,
34
36
  getByteLength,
@@ -41,6 +41,7 @@ __export(hashline_exports, {
41
41
  buildHashMap: () => buildHashMap,
42
42
  computeLineHash: () => computeLineHash,
43
43
  createHashline: () => createHashline,
44
+ detectLineEnding: () => detectLineEnding,
44
45
  formatFileWithHashes: () => formatFileWithHashes,
45
46
  getAdaptiveHashLength: () => getAdaptiveHashLength,
46
47
  getByteLength: () => getByteLength,
@@ -100,16 +101,24 @@ function computeLineHash(idx, line, hashLen = 3) {
100
101
  return hash.toString(16).padStart(hashLen, "0");
101
102
  }
102
103
  function formatFileWithHashes(content, hashLen, prefix) {
103
- const lines = content.split("\n");
104
+ const normalized = content.includes("\r\n") ? content.replace(/\r\n/g, "\n") : content;
105
+ const lines = normalized.split("\n");
104
106
  const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
105
107
  const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
106
108
  const hashes = new Array(lines.length);
107
109
  const seen = /* @__PURE__ */ new Map();
110
+ const upgraded = /* @__PURE__ */ new Set();
108
111
  for (let idx = 0; idx < lines.length; idx++) {
109
112
  const hash = computeLineHash(idx, lines[idx], effectiveLen);
110
113
  if (seen.has(hash)) {
111
114
  const longerLen = Math.min(effectiveLen + 1, 8);
115
+ const prevIdx = seen.get(hash);
116
+ if (!upgraded.has(prevIdx)) {
117
+ hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
118
+ upgraded.add(prevIdx);
119
+ }
112
120
  hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
121
+ upgraded.add(idx);
113
122
  } else {
114
123
  seen.set(hash, idx);
115
124
  hashes[idx] = hash;
@@ -127,7 +136,9 @@ function stripHashes(content, prefix) {
127
136
  hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
128
137
  stripRegexCache.set(escapedPrefix, hashLinePattern);
129
138
  }
130
- return content.split("\n").map((line) => {
139
+ const lineEnding = detectLineEnding(content);
140
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
141
+ const result = normalized.split("\n").map((line) => {
131
142
  const match = line.match(hashLinePattern);
132
143
  if (match) {
133
144
  const patchMarker = match[1] || "";
@@ -135,11 +146,13 @@ function stripHashes(content, prefix) {
135
146
  }
136
147
  return line;
137
148
  }).join("\n");
149
+ return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
138
150
  }
139
151
  function parseHashRef(ref) {
140
152
  const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
141
153
  if (!match) {
142
- throw new Error(`Invalid hash reference: "${ref}". Expected format: "<line>:<2-8 char hex>"`);
154
+ const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
155
+ throw new Error(`Invalid hash reference: "${display}". Expected format: "<line>:<2-8 char hex>"`);
143
156
  }
144
157
  return {
145
158
  line: parseInt(match[1], 10),
@@ -156,8 +169,9 @@ function normalizeHashRef(ref) {
156
169
  if (annotated) {
157
170
  return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
158
171
  }
172
+ const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
159
173
  throw new Error(
160
- `Invalid hash reference: "${ref}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
174
+ `Invalid hash reference: "${display}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
161
175
  );
162
176
  }
163
177
  function buildHashMap(content, hashLen) {
@@ -200,12 +214,14 @@ function resolveRange(startRef, endRef, content, hashLen) {
200
214
  `Invalid range: start line ${start.line} is after end line ${end.line}`
201
215
  );
202
216
  }
203
- const lines = content.split("\n");
204
- const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
217
+ const lineEnding = detectLineEnding(content);
218
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
219
+ const lines = normalized.split("\n");
220
+ const startVerify = verifyHash(start.line, start.hash, normalized, hashLen, lines);
205
221
  if (!startVerify.valid) {
206
222
  throw new Error(`Start reference invalid: ${startVerify.message}`);
207
223
  }
208
- const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
224
+ const endVerify = verifyHash(end.line, end.hash, normalized, hashLen, lines);
209
225
  if (!endVerify.valid) {
210
226
  throw new Error(`End reference invalid: ${endVerify.message}`);
211
227
  }
@@ -214,22 +230,27 @@ function resolveRange(startRef, endRef, content, hashLen) {
214
230
  startLine: start.line,
215
231
  endLine: end.line,
216
232
  lines: rangeLines,
217
- content: rangeLines.join("\n")
233
+ content: rangeLines.join(lineEnding)
218
234
  };
219
235
  }
220
236
  function replaceRange(startRef, endRef, content, replacement, hashLen) {
221
- const range = resolveRange(startRef, endRef, content, hashLen);
222
- const lines = content.split("\n");
237
+ const lineEnding = detectLineEnding(content);
238
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
239
+ const range = resolveRange(startRef, endRef, normalized, hashLen);
240
+ const lines = normalized.split("\n");
223
241
  const before = lines.slice(0, range.startLine - 1);
224
242
  const after = lines.slice(range.endLine);
225
243
  const replacementLines = replacement.split("\n");
226
- return [...before, ...replacementLines, ...after].join("\n");
244
+ const result = [...before, ...replacementLines, ...after].join("\n");
245
+ return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
227
246
  }
228
247
  function applyHashEdit(input, content, hashLen) {
248
+ const lineEnding = detectLineEnding(content);
249
+ const workContent = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
229
250
  const normalizedStart = normalizeHashRef(input.startRef);
230
251
  const start = parseHashRef(normalizedStart);
231
- const lines = content.split("\n");
232
- const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
252
+ const lines = workContent.split("\n");
253
+ const startVerify = verifyHash(start.line, start.hash, workContent, hashLen, lines);
233
254
  if (!startVerify.valid) {
234
255
  throw new Error(`Start reference invalid: ${startVerify.message}`);
235
256
  }
@@ -248,7 +269,7 @@ function applyHashEdit(input, content, hashLen) {
248
269
  operation: input.operation,
249
270
  startLine: start.line,
250
271
  endLine: start.line,
251
- content: next2
272
+ content: lineEnding === "\r\n" ? next2.replace(/\n/g, "\r\n") : next2
252
273
  };
253
274
  }
254
275
  const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
@@ -258,7 +279,7 @@ function applyHashEdit(input, content, hashLen) {
258
279
  `Invalid range: start line ${start.line} is after end line ${end.line}`
259
280
  );
260
281
  }
261
- const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
282
+ const endVerify = verifyHash(end.line, end.hash, workContent, hashLen, lines);
262
283
  if (!endVerify.valid) {
263
284
  throw new Error(`End reference invalid: ${endVerify.message}`);
264
285
  }
@@ -274,13 +295,17 @@ function applyHashEdit(input, content, hashLen) {
274
295
  operation: input.operation,
275
296
  startLine: start.line,
276
297
  endLine: end.line,
277
- content: next
298
+ content: lineEnding === "\r\n" ? next.replace(/\n/g, "\r\n") : next
278
299
  };
279
300
  }
280
301
  function matchesGlob(filePath, pattern) {
281
302
  const normalizedPath = filePath.replace(/\\/g, "/");
282
303
  const normalizedPattern = pattern.replace(/\\/g, "/");
283
- const isMatch = (0, import_picomatch.default)(normalizedPattern, { dot: true });
304
+ let isMatch = globMatcherCache.get(normalizedPattern);
305
+ if (!isMatch) {
306
+ isMatch = (0, import_picomatch.default)(normalizedPattern, { dot: true });
307
+ globMatcherCache.set(normalizedPattern, isMatch);
308
+ }
284
309
  return isMatch(normalizedPath);
285
310
  }
286
311
  function shouldExclude(filePath, patterns) {
@@ -289,6 +314,9 @@ function shouldExclude(filePath, patterns) {
289
314
  function getByteLength(content) {
290
315
  return textEncoder.encode(content).length;
291
316
  }
317
+ function detectLineEnding(content) {
318
+ return content.includes("\r\n") ? "\r\n" : "\n";
319
+ }
292
320
  function createHashline(config) {
293
321
  const resolved = resolveConfig(config);
294
322
  const cache = new HashlineCache(resolved.cacheSize);
@@ -340,7 +368,7 @@ function createHashline(config) {
340
368
  }
341
369
  };
342
370
  }
343
- var import_picomatch, DEFAULT_EXCLUDE_PATTERNS, DEFAULT_PREFIX, DEFAULT_CONFIG, modulusCache, stripRegexCache, HashlineCache, textEncoder;
371
+ var import_picomatch, DEFAULT_EXCLUDE_PATTERNS, DEFAULT_PREFIX, DEFAULT_CONFIG, modulusCache, stripRegexCache, HashlineCache, globMatcherCache, textEncoder;
344
372
  var init_hashline = __esm({
345
373
  "src/hashline.ts"() {
346
374
  "use strict";
@@ -373,7 +401,24 @@ var init_hashline = __esm({
373
401
  "**/*.exe",
374
402
  "**/*.dll",
375
403
  "**/*.so",
376
- "**/*.dylib"
404
+ "**/*.dylib",
405
+ // Sensitive credential and secret files
406
+ "**/.env",
407
+ "**/.env.*",
408
+ "**/*.pem",
409
+ "**/*.key",
410
+ "**/*.p12",
411
+ "**/*.pfx",
412
+ "**/id_rsa",
413
+ "**/id_rsa.pub",
414
+ "**/id_ed25519",
415
+ "**/id_ed25519.pub",
416
+ "**/id_ecdsa",
417
+ "**/id_ecdsa.pub",
418
+ "**/.npmrc",
419
+ "**/.netrc",
420
+ "**/credentials",
421
+ "**/credentials.json"
377
422
  ];
378
423
  DEFAULT_PREFIX = "#HL ";
379
424
  DEFAULT_CONFIG = {
@@ -447,6 +492,7 @@ var init_hashline = __esm({
447
492
  return this.cache.size;
448
493
  }
449
494
  };
495
+ globMatcherCache = /* @__PURE__ */ new Map();
450
496
  textEncoder = new TextEncoder();
451
497
  }
452
498
  });
@@ -690,23 +736,37 @@ function createHashlineEditTool(config, cache) {
690
736
  operation: import_zod.z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
691
737
  startRef: import_zod.z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
692
738
  endRef: import_zod.z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
693
- replacement: import_zod.z.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
739
+ replacement: import_zod.z.string().max(1e7).optional().describe("Replacement/inserted content. Required for replace/insert operations.")
694
740
  },
695
741
  async execute(args, context) {
696
742
  const { path, operation, startRef, endRef, replacement } = args;
697
743
  const absPath = (0, import_path2.isAbsolute)(path) ? path : (0, import_path2.resolve)(context.directory, path);
698
- let realAbs;
699
- try {
700
- realAbs = (0, import_fs2.realpathSync)(absPath);
701
- } catch {
702
- realAbs = (0, import_path2.resolve)(absPath);
703
- }
704
744
  const realDirectory = (0, import_fs2.realpathSync)((0, import_path2.resolve)(context.directory));
705
745
  const realWorktree = (0, import_fs2.realpathSync)((0, import_path2.resolve)(context.worktree));
706
746
  function isWithin(filePath, dir) {
707
747
  if (dir === import_path2.sep) return false;
748
+ if (process.platform === "win32") {
749
+ if (/^[A-Za-z]:\\$/.test(dir)) return false;
750
+ if (/^\\\\[^\\]+\\[^\\]+$/.test(dir)) return false;
751
+ }
708
752
  return filePath === dir || filePath.startsWith(dir + import_path2.sep);
709
753
  }
754
+ let realAbs;
755
+ try {
756
+ realAbs = (0, import_fs2.realpathSync)(absPath);
757
+ } catch {
758
+ const parentDir = (0, import_path2.dirname)(absPath);
759
+ let realParent;
760
+ try {
761
+ realParent = (0, import_fs2.realpathSync)(parentDir);
762
+ } catch {
763
+ throw new Error(`Access denied: cannot verify parent directory for "${path}"`);
764
+ }
765
+ if (!isWithin(realParent, realDirectory) && !isWithin(realParent, realWorktree)) {
766
+ throw new Error(`Access denied: "${path}" resolves outside the project directory`);
767
+ }
768
+ realAbs = (0, import_path2.resolve)(absPath);
769
+ }
710
770
  if (!isWithin(realAbs, realDirectory) && !isWithin(realAbs, realWorktree)) {
711
771
  throw new Error(`Access denied: "${path}" resolves outside the project directory`);
712
772
  }
@@ -719,6 +779,11 @@ function createHashlineEditTool(config, cache) {
719
779
  const reason = error instanceof Error ? error.message : String(error);
720
780
  throw new Error(`Failed to read "${displayPath}": ${reason}`);
721
781
  }
782
+ if (config.maxFileSize > 0 && getByteLength(current) > config.maxFileSize) {
783
+ throw new Error(
784
+ `File "${displayPath}" exceeds the configured maximum size (${config.maxFileSize} bytes)`
785
+ );
786
+ }
722
787
  let nextContent;
723
788
  let startLine;
724
789
  let endLine;
@@ -773,10 +838,40 @@ function createHashlineEditTool(config, cache) {
773
838
 
774
839
  // src/index.ts
775
840
  var CONFIG_FILENAME = "opencode-hashline.json";
841
+ function sanitizeConfig(raw) {
842
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
843
+ const r = raw;
844
+ const result = {};
845
+ if (Array.isArray(r.exclude)) {
846
+ result.exclude = r.exclude.filter(
847
+ (p) => typeof p === "string" && p.length <= 512
848
+ );
849
+ }
850
+ if (typeof r.maxFileSize === "number" && Number.isFinite(r.maxFileSize) && r.maxFileSize >= 0) {
851
+ result.maxFileSize = r.maxFileSize;
852
+ }
853
+ if (typeof r.hashLength === "number" && Number.isFinite(r.hashLength)) {
854
+ result.hashLength = Math.max(0, Math.min(8, Math.floor(r.hashLength)));
855
+ }
856
+ if (typeof r.cacheSize === "number" && Number.isFinite(r.cacheSize) && r.cacheSize > 0) {
857
+ result.cacheSize = Math.min(Math.floor(r.cacheSize), 1e4);
858
+ }
859
+ if (r.prefix === false) {
860
+ result.prefix = false;
861
+ } else if (typeof r.prefix === "string") {
862
+ if (/^[\x20-\x7E]{0,20}$/.test(r.prefix)) {
863
+ result.prefix = r.prefix;
864
+ }
865
+ }
866
+ if (typeof r.debug === "boolean") {
867
+ result.debug = r.debug;
868
+ }
869
+ return result;
870
+ }
776
871
  function loadConfigFile(filePath) {
777
872
  try {
778
873
  const raw = (0, import_fs3.readFileSync)(filePath, "utf-8");
779
- return JSON.parse(raw);
874
+ return sanitizeConfig(JSON.parse(raw));
780
875
  } catch {
781
876
  return void 0;
782
877
  }
@@ -3,12 +3,13 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  setDebug
6
- } from "./chunk-JOA7B5LK.js";
6
+ } from "./chunk-VPCMHCTB.js";
7
7
  import {
8
8
  HashlineCache,
9
9
  applyHashEdit,
10
+ getByteLength,
10
11
  resolveConfig
11
- } from "./chunk-DDXOFWTU.js";
12
+ } from "./chunk-I6RACR3D.js";
12
13
 
13
14
  // src/index.ts
14
15
  import { readFileSync as readFileSync2, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
@@ -18,7 +19,7 @@ import { fileURLToPath } from "url";
18
19
 
19
20
  // src/hashline-tool.ts
20
21
  import { readFileSync, realpathSync, writeFileSync } from "fs";
21
- import { isAbsolute, relative, resolve, sep } from "path";
22
+ import { dirname, isAbsolute, relative, resolve, sep } from "path";
22
23
  import { z } from "zod";
23
24
  function createHashlineEditTool(config, cache) {
24
25
  return {
@@ -28,23 +29,37 @@ function createHashlineEditTool(config, cache) {
28
29
  operation: z.enum(["replace", "delete", "insert_before", "insert_after"]).describe("Edit operation"),
29
30
  startRef: z.string().describe('Start hash reference, e.g. "5:a3f" or "#HL 5:a3f|const x = 1;"'),
30
31
  endRef: z.string().optional().describe("End hash reference for range operations. Defaults to startRef when omitted."),
31
- replacement: z.string().optional().describe("Replacement/inserted content. Required for replace/insert operations.")
32
+ replacement: z.string().max(1e7).optional().describe("Replacement/inserted content. Required for replace/insert operations.")
32
33
  },
33
34
  async execute(args, context) {
34
35
  const { path, operation, startRef, endRef, replacement } = args;
35
36
  const absPath = isAbsolute(path) ? path : resolve(context.directory, path);
36
- let realAbs;
37
- try {
38
- realAbs = realpathSync(absPath);
39
- } catch {
40
- realAbs = resolve(absPath);
41
- }
42
37
  const realDirectory = realpathSync(resolve(context.directory));
43
38
  const realWorktree = realpathSync(resolve(context.worktree));
44
39
  function isWithin(filePath, dir) {
45
40
  if (dir === sep) return false;
41
+ if (process.platform === "win32") {
42
+ if (/^[A-Za-z]:\\$/.test(dir)) return false;
43
+ if (/^\\\\[^\\]+\\[^\\]+$/.test(dir)) return false;
44
+ }
46
45
  return filePath === dir || filePath.startsWith(dir + sep);
47
46
  }
47
+ let realAbs;
48
+ try {
49
+ realAbs = realpathSync(absPath);
50
+ } catch {
51
+ const parentDir = dirname(absPath);
52
+ let realParent;
53
+ try {
54
+ realParent = realpathSync(parentDir);
55
+ } catch {
56
+ throw new Error(`Access denied: cannot verify parent directory for "${path}"`);
57
+ }
58
+ if (!isWithin(realParent, realDirectory) && !isWithin(realParent, realWorktree)) {
59
+ throw new Error(`Access denied: "${path}" resolves outside the project directory`);
60
+ }
61
+ realAbs = resolve(absPath);
62
+ }
48
63
  if (!isWithin(realAbs, realDirectory) && !isWithin(realAbs, realWorktree)) {
49
64
  throw new Error(`Access denied: "${path}" resolves outside the project directory`);
50
65
  }
@@ -57,6 +72,11 @@ function createHashlineEditTool(config, cache) {
57
72
  const reason = error instanceof Error ? error.message : String(error);
58
73
  throw new Error(`Failed to read "${displayPath}": ${reason}`);
59
74
  }
75
+ if (config.maxFileSize > 0 && getByteLength(current) > config.maxFileSize) {
76
+ throw new Error(
77
+ `File "${displayPath}" exceeds the configured maximum size (${config.maxFileSize} bytes)`
78
+ );
79
+ }
60
80
  let nextContent;
61
81
  let startLine;
62
82
  let endLine;
@@ -111,10 +131,40 @@ function createHashlineEditTool(config, cache) {
111
131
 
112
132
  // src/index.ts
113
133
  var CONFIG_FILENAME = "opencode-hashline.json";
134
+ function sanitizeConfig(raw) {
135
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
136
+ const r = raw;
137
+ const result = {};
138
+ if (Array.isArray(r.exclude)) {
139
+ result.exclude = r.exclude.filter(
140
+ (p) => typeof p === "string" && p.length <= 512
141
+ );
142
+ }
143
+ if (typeof r.maxFileSize === "number" && Number.isFinite(r.maxFileSize) && r.maxFileSize >= 0) {
144
+ result.maxFileSize = r.maxFileSize;
145
+ }
146
+ if (typeof r.hashLength === "number" && Number.isFinite(r.hashLength)) {
147
+ result.hashLength = Math.max(0, Math.min(8, Math.floor(r.hashLength)));
148
+ }
149
+ if (typeof r.cacheSize === "number" && Number.isFinite(r.cacheSize) && r.cacheSize > 0) {
150
+ result.cacheSize = Math.min(Math.floor(r.cacheSize), 1e4);
151
+ }
152
+ if (r.prefix === false) {
153
+ result.prefix = false;
154
+ } else if (typeof r.prefix === "string") {
155
+ if (/^[\x20-\x7E]{0,20}$/.test(r.prefix)) {
156
+ result.prefix = r.prefix;
157
+ }
158
+ }
159
+ if (typeof r.debug === "boolean") {
160
+ result.debug = r.debug;
161
+ }
162
+ return result;
163
+ }
114
164
  function loadConfigFile(filePath) {
115
165
  try {
116
166
  const raw = readFileSync2(filePath, "utf-8");
117
- return JSON.parse(raw);
167
+ return sanitizeConfig(JSON.parse(raw));
118
168
  } catch {
119
169
  return void 0;
120
170
  }
@@ -172,7 +222,7 @@ function createHashlinePlugin(userConfig) {
172
222
  const out = output;
173
223
  const hashLen = config.hashLength || 0;
174
224
  const prefix = config.prefix;
175
- const { formatFileWithHashes, shouldExclude, getByteLength } = await import("./hashline-GY4XM34F.js");
225
+ const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-5PFAXY3H.js");
176
226
  for (const p of out.parts ?? []) {
177
227
  if (p.type !== "file") continue;
178
228
  if (!p.url || !p.mime?.startsWith("text/")) continue;
@@ -199,7 +249,7 @@ function createHashlinePlugin(userConfig) {
199
249
  } catch {
200
250
  continue;
201
251
  }
202
- if (config.maxFileSize > 0 && getByteLength(content) > config.maxFileSize) continue;
252
+ if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
203
253
  const cached = cache.get(filePath, content);
204
254
  if (cached) {
205
255
  const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
package/dist/utils.cjs CHANGED
@@ -87,7 +87,24 @@ var DEFAULT_EXCLUDE_PATTERNS = [
87
87
  "**/*.exe",
88
88
  "**/*.dll",
89
89
  "**/*.so",
90
- "**/*.dylib"
90
+ "**/*.dylib",
91
+ // Sensitive credential and secret files
92
+ "**/.env",
93
+ "**/.env.*",
94
+ "**/*.pem",
95
+ "**/*.key",
96
+ "**/*.p12",
97
+ "**/*.pfx",
98
+ "**/id_rsa",
99
+ "**/id_rsa.pub",
100
+ "**/id_ed25519",
101
+ "**/id_ed25519.pub",
102
+ "**/id_ecdsa",
103
+ "**/id_ecdsa.pub",
104
+ "**/.npmrc",
105
+ "**/.netrc",
106
+ "**/credentials",
107
+ "**/credentials.json"
91
108
  ];
92
109
  var DEFAULT_PREFIX = "#HL ";
93
110
  var DEFAULT_CONFIG = {
@@ -147,16 +164,24 @@ function computeLineHash(idx, line, hashLen = 3) {
147
164
  return hash.toString(16).padStart(hashLen, "0");
148
165
  }
149
166
  function formatFileWithHashes(content, hashLen, prefix) {
150
- const lines = content.split("\n");
167
+ const normalized = content.includes("\r\n") ? content.replace(/\r\n/g, "\n") : content;
168
+ const lines = normalized.split("\n");
151
169
  const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
152
170
  const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
153
171
  const hashes = new Array(lines.length);
154
172
  const seen = /* @__PURE__ */ new Map();
173
+ const upgraded = /* @__PURE__ */ new Set();
155
174
  for (let idx = 0; idx < lines.length; idx++) {
156
175
  const hash = computeLineHash(idx, lines[idx], effectiveLen);
157
176
  if (seen.has(hash)) {
158
177
  const longerLen = Math.min(effectiveLen + 1, 8);
178
+ const prevIdx = seen.get(hash);
179
+ if (!upgraded.has(prevIdx)) {
180
+ hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
181
+ upgraded.add(prevIdx);
182
+ }
159
183
  hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
184
+ upgraded.add(idx);
160
185
  } else {
161
186
  seen.set(hash, idx);
162
187
  hashes[idx] = hash;
@@ -175,7 +200,9 @@ function stripHashes(content, prefix) {
175
200
  hashLinePattern = new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
176
201
  stripRegexCache.set(escapedPrefix, hashLinePattern);
177
202
  }
178
- return content.split("\n").map((line) => {
203
+ const lineEnding = detectLineEnding(content);
204
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
205
+ const result = normalized.split("\n").map((line) => {
179
206
  const match = line.match(hashLinePattern);
180
207
  if (match) {
181
208
  const patchMarker = match[1] || "";
@@ -183,11 +210,13 @@ function stripHashes(content, prefix) {
183
210
  }
184
211
  return line;
185
212
  }).join("\n");
213
+ return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
186
214
  }
187
215
  function parseHashRef(ref) {
188
216
  const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
189
217
  if (!match) {
190
- throw new Error(`Invalid hash reference: "${ref}". Expected format: "<line>:<2-8 char hex>"`);
218
+ const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
219
+ throw new Error(`Invalid hash reference: "${display}". Expected format: "<line>:<2-8 char hex>"`);
191
220
  }
192
221
  return {
193
222
  line: parseInt(match[1], 10),
@@ -204,8 +233,9 @@ function normalizeHashRef(ref) {
204
233
  if (annotated) {
205
234
  return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
206
235
  }
236
+ const display = ref.length > 100 ? `${ref.slice(0, 100)}\u2026` : ref;
207
237
  throw new Error(
208
- `Invalid hash reference: "${ref}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
238
+ `Invalid hash reference: "${display}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
209
239
  );
210
240
  }
211
241
  function buildHashMap(content, hashLen) {
@@ -248,12 +278,14 @@ function resolveRange(startRef, endRef, content, hashLen) {
248
278
  `Invalid range: start line ${start.line} is after end line ${end.line}`
249
279
  );
250
280
  }
251
- const lines = content.split("\n");
252
- const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
281
+ const lineEnding = detectLineEnding(content);
282
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
283
+ const lines = normalized.split("\n");
284
+ const startVerify = verifyHash(start.line, start.hash, normalized, hashLen, lines);
253
285
  if (!startVerify.valid) {
254
286
  throw new Error(`Start reference invalid: ${startVerify.message}`);
255
287
  }
256
- const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
288
+ const endVerify = verifyHash(end.line, end.hash, normalized, hashLen, lines);
257
289
  if (!endVerify.valid) {
258
290
  throw new Error(`End reference invalid: ${endVerify.message}`);
259
291
  }
@@ -262,22 +294,27 @@ function resolveRange(startRef, endRef, content, hashLen) {
262
294
  startLine: start.line,
263
295
  endLine: end.line,
264
296
  lines: rangeLines,
265
- content: rangeLines.join("\n")
297
+ content: rangeLines.join(lineEnding)
266
298
  };
267
299
  }
268
300
  function replaceRange(startRef, endRef, content, replacement, hashLen) {
269
- const range = resolveRange(startRef, endRef, content, hashLen);
270
- const lines = content.split("\n");
301
+ const lineEnding = detectLineEnding(content);
302
+ const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
303
+ const range = resolveRange(startRef, endRef, normalized, hashLen);
304
+ const lines = normalized.split("\n");
271
305
  const before = lines.slice(0, range.startLine - 1);
272
306
  const after = lines.slice(range.endLine);
273
307
  const replacementLines = replacement.split("\n");
274
- return [...before, ...replacementLines, ...after].join("\n");
308
+ const result = [...before, ...replacementLines, ...after].join("\n");
309
+ return lineEnding === "\r\n" ? result.replace(/\n/g, "\r\n") : result;
275
310
  }
276
311
  function applyHashEdit(input, content, hashLen) {
312
+ const lineEnding = detectLineEnding(content);
313
+ const workContent = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
277
314
  const normalizedStart = normalizeHashRef(input.startRef);
278
315
  const start = parseHashRef(normalizedStart);
279
- const lines = content.split("\n");
280
- const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
316
+ const lines = workContent.split("\n");
317
+ const startVerify = verifyHash(start.line, start.hash, workContent, hashLen, lines);
281
318
  if (!startVerify.valid) {
282
319
  throw new Error(`Start reference invalid: ${startVerify.message}`);
283
320
  }
@@ -296,7 +333,7 @@ function applyHashEdit(input, content, hashLen) {
296
333
  operation: input.operation,
297
334
  startLine: start.line,
298
335
  endLine: start.line,
299
- content: next2
336
+ content: lineEnding === "\r\n" ? next2.replace(/\n/g, "\r\n") : next2
300
337
  };
301
338
  }
302
339
  const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
@@ -306,7 +343,7 @@ function applyHashEdit(input, content, hashLen) {
306
343
  `Invalid range: start line ${start.line} is after end line ${end.line}`
307
344
  );
308
345
  }
309
- const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
346
+ const endVerify = verifyHash(end.line, end.hash, workContent, hashLen, lines);
310
347
  if (!endVerify.valid) {
311
348
  throw new Error(`End reference invalid: ${endVerify.message}`);
312
349
  }
@@ -322,7 +359,7 @@ function applyHashEdit(input, content, hashLen) {
322
359
  operation: input.operation,
323
360
  startLine: start.line,
324
361
  endLine: end.line,
325
- content: next
362
+ content: lineEnding === "\r\n" ? next.replace(/\n/g, "\r\n") : next
326
363
  };
327
364
  }
328
365
  var HashlineCache = class {
@@ -384,10 +421,15 @@ var HashlineCache = class {
384
421
  return this.cache.size;
385
422
  }
386
423
  };
424
+ var globMatcherCache = /* @__PURE__ */ new Map();
387
425
  function matchesGlob(filePath, pattern) {
388
426
  const normalizedPath = filePath.replace(/\\/g, "/");
389
427
  const normalizedPattern = pattern.replace(/\\/g, "/");
390
- const isMatch = (0, import_picomatch.default)(normalizedPattern, { dot: true });
428
+ let isMatch = globMatcherCache.get(normalizedPattern);
429
+ if (!isMatch) {
430
+ isMatch = (0, import_picomatch.default)(normalizedPattern, { dot: true });
431
+ globMatcherCache.set(normalizedPattern, isMatch);
432
+ }
391
433
  return isMatch(normalizedPath);
392
434
  }
393
435
  function shouldExclude(filePath, patterns) {
@@ -397,6 +439,9 @@ var textEncoder = new TextEncoder();
397
439
  function getByteLength(content) {
398
440
  return textEncoder.encode(content).length;
399
441
  }
442
+ function detectLineEnding(content) {
443
+ return content.includes("\r\n") ? "\r\n" : "\n";
444
+ }
400
445
  function createHashline(config) {
401
446
  const resolved = resolveConfig(config);
402
447
  const cache = new HashlineCache(resolved.cacheSize);
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  isFileReadTool
6
- } from "./chunk-JOA7B5LK.js";
6
+ } from "./chunk-VPCMHCTB.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-DDXOFWTU.js";
28
+ } from "./chunk-I6RACR3D.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.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Hashline plugin for OpenCode — content-addressable line hashing for precise AI code editing",
5
5
  "main": "dist/opencode-hashline.cjs",
6
6
  "module": "dist/opencode-hashline.js",