opencode-hashline 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ <img src="banner.jpg" alt="opencode-hashline banner" width="100%" />
4
+
3
5
  # 🔗 opencode-hashline
4
6
 
5
7
  **Content-addressable line hashing for precise AI code editing**
@@ -61,7 +63,6 @@ Hash length automatically adapts to file size to minimize collisions:
61
63
 
62
64
  | File Size | Hash Length | Possible Values |
63
65
  |-----------|:----------:|:---------------:|
64
- | ≤ 256 lines | 2 hex chars | 256 |
65
66
  | ≤ 4,096 lines | 3 hex chars | 4,096 |
66
67
  | > 4,096 lines | 4 hex chars | 65,536 |
67
68
 
@@ -94,7 +95,7 @@ Built-in LRU cache (`filePath → annotatedContent`) with configurable size (def
94
95
  Verify that a line hasn't changed since it was read — protects against race conditions:
95
96
 
96
97
  ```typescript
97
- import { verifyHash } from "opencode-hashline";
98
+ import { verifyHash } from "opencode-hashline/utils";
98
99
 
99
100
  const result = verifyHash(2, "f1c", currentContent);
100
101
  if (!result.valid) {
@@ -156,7 +157,7 @@ Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespa
156
157
  Resolve and replace ranges of lines by hash references:
157
158
 
158
159
  ```typescript
159
- import { resolveRange, replaceRange } from "opencode-hashline";
160
+ import { resolveRange, replaceRange } from "opencode-hashline/utils";
160
161
 
161
162
  // Get lines between two hash references
162
163
  const range = resolveRange("1:a3f", "3:0e7", content);
@@ -174,7 +175,7 @@ const newContent = replaceRange(
174
175
  Create custom Hashline instances with specific settings:
175
176
 
176
177
  ```typescript
177
- import { createHashline } from "opencode-hashline";
178
+ import { createHashline } from "opencode-hashline/utils";
178
179
 
179
180
  const hl = createHashline({
180
181
  exclude: ["**/node_modules/**", "**/*.min.js"],
@@ -194,7 +195,7 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
194
195
  | Option | Type | Default | Description |
195
196
  |--------|------|---------|-------------|
196
197
  | `exclude` | `string[]` | See below | Glob patterns for files to skip |
197
- | `maxFileSize` | `number` | `1_000_000` | Max file size in bytes |
198
+ | `maxFileSize` | `number` | `1_048_576` (1 MB) | Max file size in bytes |
198
199
  | `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
199
200
  | `cacheSize` | `number` | `100` | Max files in LRU cache |
200
201
  | `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
@@ -302,7 +303,7 @@ The plugin needs to determine which tools are "file-read" tools (to annotate the
302
303
  The `isFileReadTool()` function is exported for testing and advanced usage:
303
304
 
304
305
  ```typescript
305
- import { isFileReadTool } from "opencode-hashline";
306
+ import { isFileReadTool } from "opencode-hashline/utils";
306
307
 
307
308
  isFileReadTool("read_file"); // true
308
309
  isFileReadTool("mcp.read"); // true
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ <img src="banner.jpg" alt="opencode-hashline banner" width="100%" />
4
+
3
5
  # 🔗 opencode-hashline
4
6
 
5
7
  **Контентно-адресуемое хеширование строк для точного редактирования кода с помощью AI**
@@ -34,7 +36,7 @@ Hashline аннотирует каждую строку файла коротк
34
36
  #HL 3:0e7|}
35
37
  ```
36
38
 
37
- > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (2 символа для ≤256 строк, 3 символа для ≤4096 строк, 4 символа для >4096 строк). В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
39
+ > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (3 символа для ≤4096 строк, 4 символа для >4096 строк). Минимальная длина хеша — 3 символа. В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
38
40
 
39
41
  AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
40
42
 
@@ -61,7 +63,6 @@ Hashline адресует каждую строку уникальным хеш
61
63
 
62
64
  | Размер файла | Длина хеша | Возможных значений |
63
65
  |-------------|:----------:|:------------------:|
64
- | ≤ 256 строк | 2 hex-символа | 256 |
65
66
  | ≤ 4 096 строк | 3 hex-символа | 4 096 |
66
67
  | > 4 096 строк | 4 hex-символа | 65 536 |
67
68
 
@@ -94,7 +95,7 @@ const hl = createHashline({ prefix: false });
94
95
  Проверка того, что строка не изменилась с момента чтения — защита от race conditions:
95
96
 
96
97
  ```typescript
97
- import { verifyHash } from "opencode-hashline";
98
+ import { verifyHash } from "opencode-hashline/utils";
98
99
 
99
100
  const result = verifyHash(2, "f1c", currentContent);
100
101
  if (!result.valid) {
@@ -156,7 +157,7 @@ const result = applyHashEdit(
156
157
  Резолвинг и замена диапазонов строк по хеш-ссылкам:
157
158
 
158
159
  ```typescript
159
- import { resolveRange, replaceRange } from "opencode-hashline";
160
+ import { resolveRange, replaceRange } from "opencode-hashline/utils";
160
161
 
161
162
  // Получить строки между двумя хеш-ссылками
162
163
  const range = resolveRange("1:a3f", "3:0e7", content);
@@ -174,7 +175,7 @@ const newContent = replaceRange(
174
175
  Создание кастомных экземпляров Hashline с определёнными настройками:
175
176
 
176
177
  ```typescript
177
- import { createHashline } from "opencode-hashline";
178
+ import { createHashline } from "opencode-hashline/utils";
178
179
 
179
180
  const hl = createHashline({
180
181
  exclude: ["**/node_modules/**", "**/*.min.js"],
@@ -194,7 +195,7 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
194
195
  | Параметр | Тип | По умолчанию | Описание |
195
196
  |----------|-----|:------------:|----------|
196
197
  | `exclude` | `string[]` | См. ниже | Glob-паттерны для исключения файлов |
197
- | `maxFileSize` | `number` | `1_000_000` | Макс. размер файла в байтах |
198
+ | `maxFileSize` | `number` | `1_048_576` (1 МБ) | Макс. размер файла в байтах |
198
199
  | `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
199
200
  | `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
200
201
  | `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
package/README.ru.md CHANGED
@@ -34,7 +34,7 @@ Hashline аннотирует каждую строку файла коротк
34
34
  #HL 3:0e7|}
35
35
  ```
36
36
 
37
- > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (2 символа для ≤256 строк, 3 символа для ≤4096 строк, 4 символа для >4096 строк). В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
37
+ > **Примечание:** Длина хеша адаптивная — она зависит от размера файла (3 символа для ≤4096 строк, 4 символа для >4096 строк). Минимальная длина хеша — 3 символа. В примерах ниже используются 3-символьные хеши. Префикс `#HL ` защищает от ложных срабатываний при удалении хешей и является настраиваемым.
38
38
 
39
39
  AI-модель может ссылаться на строки по их хеш-тегам для точного редактирования:
40
40
 
@@ -56,7 +56,6 @@ AI-модель может ссылаться на строки по их хеш
56
56
 
57
57
  | Размер файла | Длина хеша | Возможных значений |
58
58
  |-------------|:----------:|:------------------:|
59
- | ≤ 256 строк | 2 hex-символа | 256 |
60
59
  | ≤ 4 096 строк | 3 hex-символа | 4 096 |
61
60
  | > 4 096 строк | 4 hex-символа | 65 536 |
62
61
 
@@ -89,7 +88,7 @@ const hl = createHashline({ prefix: false });
89
88
  Проверка того, что строка не изменилась с момента чтения — защита от race conditions:
90
89
 
91
90
  ```typescript
92
- import { verifyHash } from "opencode-hashline";
91
+ import { verifyHash } from "opencode-hashline/utils";
93
92
 
94
93
  const result = verifyHash(2, "f1c", currentContent);
95
94
  if (!result.valid) {
@@ -108,7 +107,7 @@ if (!result.valid) {
108
107
  Резолвинг и замена диапазонов строк по хеш-ссылкам:
109
108
 
110
109
  ```typescript
111
- import { resolveRange, replaceRange } from "opencode-hashline";
110
+ import { resolveRange, replaceRange } from "opencode-hashline/utils";
112
111
 
113
112
  // Получить строки между двумя хеш-ссылками
114
113
  const range = resolveRange("1:a3f", "3:0e7", content);
@@ -126,7 +125,7 @@ const newContent = replaceRange(
126
125
  Создание кастомных экземпляров Hashline с определёнными настройками:
127
126
 
128
127
  ```typescript
129
- import { createHashline } from "opencode-hashline";
128
+ import { createHashline } from "opencode-hashline/utils";
130
129
 
131
130
  const hl = createHashline({
132
131
  exclude: ["**/node_modules/**", "**/*.min.js"],
@@ -146,10 +145,12 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
146
145
  | Параметр | Тип | По умолчанию | Описание |
147
146
  |----------|-----|:------------:|----------|
148
147
  | `exclude` | `string[]` | См. ниже | Glob-паттерны для исключения файлов |
149
- | `maxFileSize` | `number` | `1_000_000` | Макс. размер файла в байтах |
148
+ | `maxFileSize` | `number` | `1_048_576` (1 МБ) | Макс. размер файла в байтах |
150
149
  | `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
151
150
  | `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
152
151
  | `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
152
+ | `fileRev` | `boolean` | `true` | Включать ревизию файла (`#HL REV:...`) в аннотации |
153
+ | `safeReapply` | `boolean` | `false` | Автоматический поиск перемещённых строк по хешу |
153
154
 
154
155
  Паттерны исключения по умолчанию: lock-файлы, `node_modules`, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
155
156
 
@@ -199,23 +199,33 @@ function formatFileWithHashes(content, hashLen, prefix, includeFileRev) {
199
199
  const lines = normalized.split("\n");
200
200
  const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
201
201
  const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
202
+ const hashLens = new Array(lines.length).fill(effectiveLen);
202
203
  const hashes = new Array(lines.length);
203
- const seen = /* @__PURE__ */ new Map();
204
- const upgraded = /* @__PURE__ */ new Set();
205
204
  for (let idx = 0; idx < lines.length; idx++) {
206
- const hash = computeLineHash(idx, lines[idx], effectiveLen);
207
- if (seen.has(hash)) {
208
- const longerLen = Math.min(effectiveLen + 1, 8);
209
- const prevIdx = seen.get(hash);
210
- if (!upgraded.has(prevIdx)) {
211
- hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
212
- upgraded.add(prevIdx);
205
+ hashes[idx] = computeLineHash(idx, lines[idx], effectiveLen);
206
+ }
207
+ let hasCollisions = true;
208
+ while (hasCollisions) {
209
+ hasCollisions = false;
210
+ const seen = /* @__PURE__ */ new Map();
211
+ for (let idx = 0; idx < lines.length; idx++) {
212
+ const h = hashes[idx];
213
+ const group = seen.get(h);
214
+ if (group) {
215
+ group.push(idx);
216
+ } else {
217
+ seen.set(h, [idx]);
218
+ }
219
+ }
220
+ for (const [, group] of seen) {
221
+ if (group.length < 2) continue;
222
+ for (const idx of group) {
223
+ const newLen = Math.min(hashLens[idx] + 1, 8);
224
+ if (newLen === hashLens[idx]) continue;
225
+ hashLens[idx] = newLen;
226
+ hashes[idx] = computeLineHash(idx, lines[idx], newLen);
227
+ hasCollisions = true;
213
228
  }
214
- hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
215
- upgraded.add(idx);
216
- } else {
217
- seen.set(hash, idx);
218
- hashes[idx] = hash;
219
229
  }
220
230
  }
221
231
  const annotatedLines = lines.map((line, idx) => {
@@ -4,7 +4,7 @@ import {
4
4
  resolveConfig,
5
5
  shouldExclude,
6
6
  stripHashes
7
- } from "./chunk-DOR4YDIS.js";
7
+ } from "./chunk-GKXY5ZBM.js";
8
8
 
9
9
  // src/hooks.ts
10
10
  import { appendFileSync } from "fs";
@@ -25,7 +25,7 @@ import {
25
25
  stripHashes,
26
26
  verifyFileRev,
27
27
  verifyHash
28
- } from "./chunk-DOR4YDIS.js";
28
+ } from "./chunk-GKXY5ZBM.js";
29
29
  export {
30
30
  DEFAULT_CONFIG,
31
31
  DEFAULT_EXCLUDE_PATTERNS,
@@ -154,23 +154,33 @@ function formatFileWithHashes(content, hashLen, prefix, includeFileRev) {
154
154
  const lines = normalized.split("\n");
155
155
  const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
156
156
  const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
157
+ const hashLens = new Array(lines.length).fill(effectiveLen);
157
158
  const hashes = new Array(lines.length);
158
- const seen = /* @__PURE__ */ new Map();
159
- const upgraded = /* @__PURE__ */ new Set();
160
159
  for (let idx = 0; idx < lines.length; idx++) {
161
- const hash = computeLineHash(idx, lines[idx], effectiveLen);
162
- if (seen.has(hash)) {
163
- const longerLen = Math.min(effectiveLen + 1, 8);
164
- const prevIdx = seen.get(hash);
165
- if (!upgraded.has(prevIdx)) {
166
- hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
167
- upgraded.add(prevIdx);
160
+ hashes[idx] = computeLineHash(idx, lines[idx], effectiveLen);
161
+ }
162
+ let hasCollisions = true;
163
+ while (hasCollisions) {
164
+ hasCollisions = false;
165
+ const seen = /* @__PURE__ */ new Map();
166
+ for (let idx = 0; idx < lines.length; idx++) {
167
+ const h = hashes[idx];
168
+ const group = seen.get(h);
169
+ if (group) {
170
+ group.push(idx);
171
+ } else {
172
+ seen.set(h, [idx]);
173
+ }
174
+ }
175
+ for (const [, group] of seen) {
176
+ if (group.length < 2) continue;
177
+ for (const idx of group) {
178
+ const newLen = Math.min(hashLens[idx] + 1, 8);
179
+ if (newLen === hashLens[idx]) continue;
180
+ hashLens[idx] = newLen;
181
+ hashes[idx] = computeLineHash(idx, lines[idx], newLen);
182
+ hasCollisions = true;
168
183
  }
169
- hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
170
- upgraded.add(idx);
171
- } else {
172
- seen.set(hash, idx);
173
- hashes[idx] = hash;
174
184
  }
175
185
  }
176
186
  const annotatedLines = lines.map((line, idx) => {
@@ -707,6 +717,7 @@ module.exports = __toCommonJS(src_exports);
707
717
  var import_fs3 = require("fs");
708
718
  var import_path3 = require("path");
709
719
  var import_os2 = require("os");
720
+ var import_crypto = require("crypto");
710
721
  var import_url = require("url");
711
722
 
712
723
  // src/hooks.ts
@@ -1067,6 +1078,33 @@ ${error.toDiagnostic()}`);
1067
1078
 
1068
1079
  // src/index.ts
1069
1080
  var CONFIG_FILENAME = "opencode-hashline.json";
1081
+ var tempDirs = /* @__PURE__ */ new Set();
1082
+ var exitListenerRegistered = false;
1083
+ function registerTempDir(dir) {
1084
+ tempDirs.add(dir);
1085
+ if (!exitListenerRegistered) {
1086
+ exitListenerRegistered = true;
1087
+ process.on("exit", () => {
1088
+ for (const d of tempDirs) {
1089
+ try {
1090
+ (0, import_fs3.rmSync)(d, { recursive: true, force: true });
1091
+ } catch {
1092
+ }
1093
+ }
1094
+ });
1095
+ }
1096
+ }
1097
+ function writeTempFile(tempDir, content) {
1098
+ const name = `hl-${(0, import_crypto.randomBytes)(16).toString("hex")}.txt`;
1099
+ const tmpPath = (0, import_path3.join)(tempDir, name);
1100
+ const fd = (0, import_fs3.openSync)(tmpPath, import_fs3.constants.O_WRONLY | import_fs3.constants.O_CREAT | import_fs3.constants.O_EXCL, 384);
1101
+ try {
1102
+ (0, import_fs3.writeFileSync)(fd, content, "utf-8");
1103
+ } finally {
1104
+ (0, import_fs3.closeSync)(fd);
1105
+ }
1106
+ return tmpPath;
1107
+ }
1070
1108
  function sanitizeConfig(raw) {
1071
1109
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
1072
1110
  const r = raw;
@@ -1141,17 +1179,8 @@ function createHashlinePlugin(userConfig) {
1141
1179
  } catch {
1142
1180
  }
1143
1181
  }
1144
- const tempFiles = /* @__PURE__ */ new Set();
1145
- const cleanupTempFiles = () => {
1146
- for (const f of tempFiles) {
1147
- try {
1148
- (0, import_fs3.unlinkSync)(f);
1149
- } catch {
1150
- }
1151
- }
1152
- tempFiles.clear();
1153
- };
1154
- process.on("exit", cleanupTempFiles);
1182
+ const instanceTmpDir = (0, import_fs3.mkdtempSync)((0, import_path3.join)((0, import_os2.tmpdir)(), "hashline-"));
1183
+ registerTempDir(instanceTmpDir);
1155
1184
  return {
1156
1185
  tool: {
1157
1186
  hashline_edit: createHashlineEditTool(config, cache)
@@ -1194,9 +1223,7 @@ function createHashlinePlugin(userConfig) {
1194
1223
  if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
1195
1224
  const cached = cache.get(filePath, content);
1196
1225
  if (cached) {
1197
- const tmpPath2 = (0, import_path3.join)((0, import_os2.tmpdir)(), `hashline-${p.id}.txt`);
1198
- (0, import_fs3.writeFileSync)(tmpPath2, cached, "utf-8");
1199
- tempFiles.add(tmpPath2);
1226
+ const tmpPath2 = writeTempFile(instanceTmpDir, cached);
1200
1227
  p.url = `file://${tmpPath2}`;
1201
1228
  if (config.debug) {
1202
1229
  try {
@@ -1207,10 +1234,9 @@ function createHashlinePlugin(userConfig) {
1207
1234
  }
1208
1235
  continue;
1209
1236
  }
1210
- const annotated = formatFileWithHashes2(content, hashLen || void 0, prefix);
1237
+ const annotated = formatFileWithHashes2(content, hashLen || void 0, prefix, config.fileRev);
1211
1238
  cache.set(filePath, content, annotated);
1212
- const tmpPath = (0, import_path3.join)((0, import_os2.tmpdir)(), `hashline-${p.id}.txt`);
1213
- (0, import_fs3.writeFileSync)(tmpPath, annotated, "utf-8");
1239
+ const tmpPath = writeTempFile(instanceTmpDir, annotated);
1214
1240
  p.url = `file://${tmpPath}`;
1215
1241
  if (config.debug) {
1216
1242
  try {
@@ -3,19 +3,20 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  setDebug
6
- } from "./chunk-7KUPGN4M.js";
6
+ } from "./chunk-VSVVWPET.js";
7
7
  import {
8
8
  HashlineCache,
9
9
  HashlineError,
10
10
  applyHashEdit,
11
11
  getByteLength,
12
12
  resolveConfig
13
- } from "./chunk-DOR4YDIS.js";
13
+ } from "./chunk-GKXY5ZBM.js";
14
14
 
15
15
  // src/index.ts
16
- import { readFileSync as readFileSync2, realpathSync as realpathSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
16
+ import { readFileSync as readFileSync2, realpathSync as realpathSync2, writeFileSync as writeFileSync2, mkdtempSync, openSync, closeSync, rmSync, constants as fsConstants } from "fs";
17
17
  import { join, resolve as resolve2, sep as sep2 } from "path";
18
18
  import { homedir, tmpdir } from "os";
19
+ import { randomBytes } from "crypto";
19
20
  import { fileURLToPath } from "url";
20
21
 
21
22
  // src/hashline-tool.ts
@@ -140,6 +141,33 @@ ${error.toDiagnostic()}`);
140
141
 
141
142
  // src/index.ts
142
143
  var CONFIG_FILENAME = "opencode-hashline.json";
144
+ var tempDirs = /* @__PURE__ */ new Set();
145
+ var exitListenerRegistered = false;
146
+ function registerTempDir(dir) {
147
+ tempDirs.add(dir);
148
+ if (!exitListenerRegistered) {
149
+ exitListenerRegistered = true;
150
+ process.on("exit", () => {
151
+ for (const d of tempDirs) {
152
+ try {
153
+ rmSync(d, { recursive: true, force: true });
154
+ } catch {
155
+ }
156
+ }
157
+ });
158
+ }
159
+ }
160
+ function writeTempFile(tempDir, content) {
161
+ const name = `hl-${randomBytes(16).toString("hex")}.txt`;
162
+ const tmpPath = join(tempDir, name);
163
+ const fd = openSync(tmpPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
164
+ try {
165
+ writeFileSync2(fd, content, "utf-8");
166
+ } finally {
167
+ closeSync(fd);
168
+ }
169
+ return tmpPath;
170
+ }
143
171
  function sanitizeConfig(raw) {
144
172
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
145
173
  const r = raw;
@@ -214,17 +242,8 @@ function createHashlinePlugin(userConfig) {
214
242
  } catch {
215
243
  }
216
244
  }
217
- const tempFiles = /* @__PURE__ */ new Set();
218
- const cleanupTempFiles = () => {
219
- for (const f of tempFiles) {
220
- try {
221
- unlinkSync(f);
222
- } catch {
223
- }
224
- }
225
- tempFiles.clear();
226
- };
227
- process.on("exit", cleanupTempFiles);
245
+ const instanceTmpDir = mkdtempSync(join(tmpdir(), "hashline-"));
246
+ registerTempDir(instanceTmpDir);
228
247
  return {
229
248
  tool: {
230
249
  hashline_edit: createHashlineEditTool(config, cache)
@@ -237,7 +256,7 @@ function createHashlinePlugin(userConfig) {
237
256
  const out = output;
238
257
  const hashLen = config.hashLength || 0;
239
258
  const prefix = config.prefix;
240
- const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-MGDEWZ77.js");
259
+ const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-37RYBX5A.js");
241
260
  for (const p of out.parts ?? []) {
242
261
  if (p.type !== "file") continue;
243
262
  if (!p.url || !p.mime?.startsWith("text/")) continue;
@@ -267,9 +286,7 @@ function createHashlinePlugin(userConfig) {
267
286
  if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
268
287
  const cached = cache.get(filePath, content);
269
288
  if (cached) {
270
- const tmpPath2 = join(tmpdir(), `hashline-${p.id}.txt`);
271
- writeFileSync2(tmpPath2, cached, "utf-8");
272
- tempFiles.add(tmpPath2);
289
+ const tmpPath2 = writeTempFile(instanceTmpDir, cached);
273
290
  p.url = `file://${tmpPath2}`;
274
291
  if (config.debug) {
275
292
  try {
@@ -280,10 +297,9 @@ function createHashlinePlugin(userConfig) {
280
297
  }
281
298
  continue;
282
299
  }
283
- const annotated = formatFileWithHashes(content, hashLen || void 0, prefix);
300
+ const annotated = formatFileWithHashes(content, hashLen || void 0, prefix, config.fileRev);
284
301
  cache.set(filePath, content, annotated);
285
- const tmpPath = join(tmpdir(), `hashline-${p.id}.txt`);
286
- writeFileSync2(tmpPath, annotated, "utf-8");
302
+ const tmpPath = writeTempFile(instanceTmpDir, annotated);
287
303
  p.url = `file://${tmpPath}`;
288
304
  if (config.debug) {
289
305
  try {
package/dist/utils.cjs CHANGED
@@ -263,23 +263,33 @@ function formatFileWithHashes(content, hashLen, prefix, includeFileRev) {
263
263
  const lines = normalized.split("\n");
264
264
  const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
265
265
  const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
266
+ const hashLens = new Array(lines.length).fill(effectiveLen);
266
267
  const hashes = new Array(lines.length);
267
- const seen = /* @__PURE__ */ new Map();
268
- const upgraded = /* @__PURE__ */ new Set();
269
268
  for (let idx = 0; idx < lines.length; idx++) {
270
- const hash = computeLineHash(idx, lines[idx], effectiveLen);
271
- if (seen.has(hash)) {
272
- const longerLen = Math.min(effectiveLen + 1, 8);
273
- const prevIdx = seen.get(hash);
274
- if (!upgraded.has(prevIdx)) {
275
- hashes[prevIdx] = computeLineHash(prevIdx, lines[prevIdx], longerLen);
276
- upgraded.add(prevIdx);
269
+ hashes[idx] = computeLineHash(idx, lines[idx], effectiveLen);
270
+ }
271
+ let hasCollisions = true;
272
+ while (hasCollisions) {
273
+ hasCollisions = false;
274
+ const seen = /* @__PURE__ */ new Map();
275
+ for (let idx = 0; idx < lines.length; idx++) {
276
+ const h = hashes[idx];
277
+ const group = seen.get(h);
278
+ if (group) {
279
+ group.push(idx);
280
+ } else {
281
+ seen.set(h, [idx]);
282
+ }
283
+ }
284
+ for (const [, group] of seen) {
285
+ if (group.length < 2) continue;
286
+ for (const idx of group) {
287
+ const newLen = Math.min(hashLens[idx] + 1, 8);
288
+ if (newLen === hashLens[idx]) continue;
289
+ hashLens[idx] = newLen;
290
+ hashes[idx] = computeLineHash(idx, lines[idx], newLen);
291
+ hasCollisions = true;
277
292
  }
278
- hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
279
- upgraded.add(idx);
280
- } else {
281
- seen.set(hash, idx);
282
- hashes[idx] = hash;
283
293
  }
284
294
  }
285
295
  const annotatedLines = lines.map((line, idx) => {
package/dist/utils.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  createFileReadAfterHook,
4
4
  createSystemPromptHook,
5
5
  isFileReadTool
6
- } from "./chunk-7KUPGN4M.js";
6
+ } from "./chunk-VSVVWPET.js";
7
7
  import {
8
8
  DEFAULT_CONFIG,
9
9
  DEFAULT_EXCLUDE_PATTERNS,
@@ -30,7 +30,7 @@ import {
30
30
  stripHashes,
31
31
  verifyFileRev,
32
32
  verifyHash
33
- } from "./chunk-DOR4YDIS.js";
33
+ } from "./chunk-GKXY5ZBM.js";
34
34
  export {
35
35
  DEFAULT_CONFIG,
36
36
  DEFAULT_EXCLUDE_PATTERNS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-hashline",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",