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 +7 -6
- package/README.md +7 -6
- package/README.ru.md +7 -6
- package/dist/{chunk-DOR4YDIS.js → chunk-GKXY5ZBM.js} +24 -14
- package/dist/{chunk-7KUPGN4M.js → chunk-VSVVWPET.js} +1 -1
- package/dist/{hashline-MGDEWZ77.js → hashline-37RYBX5A.js} +1 -1
- package/dist/opencode-hashline.cjs +57 -31
- package/dist/opencode-hashline.js +37 -21
- package/dist/utils.cjs +24 -14
- package/dist/utils.js +2 -2
- package/package.json +1 -1
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` | `
|
|
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
|
-
> **Примечание:** Длина хеша адаптивная — она зависит от размера файла (
|
|
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` | `
|
|
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
|
-
> **Примечание:** Длина хеша адаптивная — она зависит от размера файла (
|
|
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` | `
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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) => {
|
|
@@ -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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
1145
|
-
|
|
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 = (
|
|
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 = (
|
|
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-
|
|
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-
|
|
13
|
+
} from "./chunk-GKXY5ZBM.js";
|
|
14
14
|
|
|
15
15
|
// src/index.ts
|
|
16
|
-
import { readFileSync as readFileSync2, realpathSync as realpathSync2,
|
|
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
|
|
218
|
-
|
|
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-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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-
|
|
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-
|
|
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.
|
|
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",
|