opencode-hashline 1.3.0 → 1.3.2
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-7KUPGN4M.js → chunk-3ED7MDEC.js} +23 -14
- package/dist/{chunk-DOR4YDIS.js → chunk-C323JLG3.js} +49 -22
- package/dist/{hashline-MGDEWZ77.js → hashline-DDPVX355.js} +1 -1
- package/dist/{hashline-A7k2yn3G.d.cts → hashline-DWndArr4.d.cts} +6 -1
- package/dist/{hashline-A7k2yn3G.d.ts → hashline-DWndArr4.d.ts} +6 -1
- package/dist/opencode-hashline.cjs +112 -61
- package/dist/opencode-hashline.d.cts +10 -3
- package/dist/opencode-hashline.d.ts +10 -3
- package/dist/opencode-hashline.js +42 -28
- package/dist/utils.cjs +71 -35
- package/dist/utils.d.cts +2 -2
- package/dist/utils.d.ts +2 -2
- 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
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
resolveConfig,
|
|
5
5
|
shouldExclude,
|
|
6
6
|
stripHashes
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-C323JLG3.js";
|
|
8
8
|
|
|
9
9
|
// src/hooks.ts
|
|
10
10
|
import { appendFileSync } from "fs";
|
|
@@ -12,18 +12,22 @@ import { join } from "path";
|
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
var DEBUG_LOG = join(homedir(), ".config", "opencode", "hashline-debug.log");
|
|
14
14
|
var MAX_PROCESSED_IDS = 1e4;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
var BoundedSet = class {
|
|
16
|
+
constructor(maxSize) {
|
|
17
|
+
this.maxSize = maxSize;
|
|
18
|
+
}
|
|
19
|
+
set = /* @__PURE__ */ new Set();
|
|
20
|
+
has(value) {
|
|
21
|
+
return this.set.has(value);
|
|
22
|
+
}
|
|
23
|
+
add(value) {
|
|
24
|
+
if (this.set.size >= this.maxSize) {
|
|
25
|
+
const first = this.set.values().next().value;
|
|
26
|
+
if (first !== void 0) this.set.delete(first);
|
|
22
27
|
}
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
}
|
|
28
|
+
this.set.add(value);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
27
31
|
var debugEnabled = false;
|
|
28
32
|
function setDebug(enabled) {
|
|
29
33
|
debugEnabled = enabled;
|
|
@@ -58,7 +62,7 @@ function createFileReadAfterHook(cache, config) {
|
|
|
58
62
|
const resolved = config ?? resolveConfig();
|
|
59
63
|
const hashLen = resolved.hashLength || 0;
|
|
60
64
|
const prefix = resolved.prefix;
|
|
61
|
-
const processedCallIds =
|
|
65
|
+
const processedCallIds = new BoundedSet(MAX_PROCESSED_IDS);
|
|
62
66
|
return async (input, output) => {
|
|
63
67
|
debug("tool.execute.after:", input.tool, "args:", input.args);
|
|
64
68
|
if (input.callID) {
|
|
@@ -67,6 +71,8 @@ function createFileReadAfterHook(cache, config) {
|
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
73
|
processedCallIds.add(input.callID);
|
|
74
|
+
} else {
|
|
75
|
+
debug("no callID \u2014 deduplication disabled for this call");
|
|
70
76
|
}
|
|
71
77
|
if (!isFileReadTool(input.tool, input.args)) {
|
|
72
78
|
debug("skipped: not a file-read tool");
|
|
@@ -105,13 +111,16 @@ function createFileReadAfterHook(cache, config) {
|
|
|
105
111
|
function createFileEditBeforeHook(config) {
|
|
106
112
|
const resolved = config ?? resolveConfig();
|
|
107
113
|
const prefix = resolved.prefix;
|
|
108
|
-
const processedCallIds =
|
|
114
|
+
const processedCallIds = new BoundedSet(MAX_PROCESSED_IDS);
|
|
109
115
|
return async (input, output) => {
|
|
110
116
|
if (input.callID) {
|
|
111
117
|
if (processedCallIds.has(input.callID)) {
|
|
118
|
+
debug("skipped: duplicate callID (edit)", input.callID);
|
|
112
119
|
return;
|
|
113
120
|
}
|
|
114
121
|
processedCallIds.add(input.callID);
|
|
122
|
+
} else {
|
|
123
|
+
debug("no callID \u2014 deduplication disabled for this edit call");
|
|
115
124
|
}
|
|
116
125
|
const toolName = input.tool.toLowerCase();
|
|
117
126
|
const isFileEdit = FILE_EDIT_TOOLS.some(
|
|
@@ -199,23 +199,47 @@ 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 dirtyIndices = null;
|
|
208
|
+
let hasCollisions = true;
|
|
209
|
+
while (hasCollisions) {
|
|
210
|
+
hasCollisions = false;
|
|
211
|
+
const seen = /* @__PURE__ */ new Map();
|
|
212
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
213
|
+
const h = hashes[idx];
|
|
214
|
+
const group = seen.get(h);
|
|
215
|
+
if (group) {
|
|
216
|
+
group.push(idx);
|
|
217
|
+
} else {
|
|
218
|
+
seen.set(h, [idx]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const nextDirty = /* @__PURE__ */ new Set();
|
|
222
|
+
for (const [, group] of seen) {
|
|
223
|
+
if (group.length < 2) continue;
|
|
224
|
+
if (dirtyIndices !== null && !group.some((idx) => dirtyIndices.has(idx))) continue;
|
|
225
|
+
for (const idx of group) {
|
|
226
|
+
const newLen = Math.min(hashLens[idx] + 1, 8);
|
|
227
|
+
if (newLen === hashLens[idx]) continue;
|
|
228
|
+
hashLens[idx] = newLen;
|
|
229
|
+
hashes[idx] = computeLineHash(idx, lines[idx], newLen);
|
|
230
|
+
nextDirty.add(idx);
|
|
231
|
+
hasCollisions = true;
|
|
213
232
|
}
|
|
214
|
-
|
|
215
|
-
|
|
233
|
+
}
|
|
234
|
+
dirtyIndices = nextDirty;
|
|
235
|
+
}
|
|
236
|
+
const finalSeen = /* @__PURE__ */ new Map();
|
|
237
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
238
|
+
const existing = finalSeen.get(hashes[idx]);
|
|
239
|
+
if (existing !== void 0) {
|
|
240
|
+
hashes[idx] = `${hashes[idx]}${idx.toString(16)}`;
|
|
216
241
|
} else {
|
|
217
|
-
|
|
218
|
-
hashes[idx] = hash;
|
|
242
|
+
finalSeen.set(hashes[idx], idx);
|
|
219
243
|
}
|
|
220
244
|
}
|
|
221
245
|
const annotatedLines = lines.map((line, idx) => {
|
|
@@ -231,12 +255,16 @@ var stripRegexCache = /* @__PURE__ */ new Map();
|
|
|
231
255
|
function stripHashes(content, prefix) {
|
|
232
256
|
const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
|
|
233
257
|
const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
234
|
-
let
|
|
235
|
-
if (!
|
|
236
|
-
|
|
237
|
-
|
|
258
|
+
let cached = stripRegexCache.get(escapedPrefix);
|
|
259
|
+
if (!cached) {
|
|
260
|
+
cached = {
|
|
261
|
+
hashLine: new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`),
|
|
262
|
+
rev: new RegExp(`^${escapedPrefix}REV:[0-9a-f]{8}$`)
|
|
263
|
+
};
|
|
264
|
+
stripRegexCache.set(escapedPrefix, cached);
|
|
238
265
|
}
|
|
239
|
-
const
|
|
266
|
+
const hashLinePattern = cached.hashLine;
|
|
267
|
+
const revPattern = cached.rev;
|
|
240
268
|
const lineEnding = detectLineEnding(content);
|
|
241
269
|
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
242
270
|
const result = normalized.split("\n").filter((line) => !revPattern.test(line)).map((line) => {
|
|
@@ -385,10 +413,10 @@ function resolveRange(startRef, endRef, content, hashLen, safeReapply) {
|
|
|
385
413
|
content: rangeLines.join(lineEnding)
|
|
386
414
|
};
|
|
387
415
|
}
|
|
388
|
-
function replaceRange(startRef, endRef, content, replacement, hashLen) {
|
|
416
|
+
function replaceRange(startRef, endRef, content, replacement, hashLen, safeReapply) {
|
|
389
417
|
const lineEnding = detectLineEnding(content);
|
|
390
418
|
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
391
|
-
const range = resolveRange(startRef, endRef, normalized, hashLen);
|
|
419
|
+
const range = resolveRange(startRef, endRef, normalized, hashLen, safeReapply);
|
|
392
420
|
const lines = normalized.split("\n");
|
|
393
421
|
const before = lines.slice(0, range.startLine - 1);
|
|
394
422
|
const after = lines.slice(range.endLine);
|
|
@@ -558,9 +586,8 @@ function matchesGlob(filePath, pattern) {
|
|
|
558
586
|
function shouldExclude(filePath, patterns) {
|
|
559
587
|
return patterns.some((pattern) => matchesGlob(filePath, pattern));
|
|
560
588
|
}
|
|
561
|
-
var textEncoder = new TextEncoder();
|
|
562
589
|
function getByteLength(content) {
|
|
563
|
-
return
|
|
590
|
+
return Buffer.byteLength(content, "utf-8");
|
|
564
591
|
}
|
|
565
592
|
function detectLineEnding(content) {
|
|
566
593
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -262,7 +262,7 @@ declare function resolveRange(startRef: string, endRef: string, content: string,
|
|
|
262
262
|
* @param hashLen - override hash length (0 or undefined = use hash.length from ref)
|
|
263
263
|
* @returns new file content with the range replaced
|
|
264
264
|
*/
|
|
265
|
-
declare function replaceRange(startRef: string, endRef: string, content: string, replacement: string, hashLen?: number): string;
|
|
265
|
+
declare function replaceRange(startRef: string, endRef: string, content: string, replacement: string, hashLen?: number, safeReapply?: boolean): string;
|
|
266
266
|
/**
|
|
267
267
|
* Apply a hash-aware edit operation directly against file content.
|
|
268
268
|
*
|
|
@@ -308,6 +308,11 @@ declare function matchesGlob(filePath: string, pattern: string): boolean;
|
|
|
308
308
|
* Check if a file path should be excluded based on config patterns.
|
|
309
309
|
*/
|
|
310
310
|
declare function shouldExclude(filePath: string, patterns: string[]): boolean;
|
|
311
|
+
/**
|
|
312
|
+
* Get the UTF-8 byte length of a string.
|
|
313
|
+
* Uses TextEncoder for accurate UTF-8 byte counting.
|
|
314
|
+
* This correctly handles multi-byte characters (Cyrillic, CJK, emoji, etc.).
|
|
315
|
+
*/
|
|
311
316
|
declare function getByteLength(content: string): number;
|
|
312
317
|
/**
|
|
313
318
|
* A Hashline instance with custom configuration.
|
|
@@ -262,7 +262,7 @@ declare function resolveRange(startRef: string, endRef: string, content: string,
|
|
|
262
262
|
* @param hashLen - override hash length (0 or undefined = use hash.length from ref)
|
|
263
263
|
* @returns new file content with the range replaced
|
|
264
264
|
*/
|
|
265
|
-
declare function replaceRange(startRef: string, endRef: string, content: string, replacement: string, hashLen?: number): string;
|
|
265
|
+
declare function replaceRange(startRef: string, endRef: string, content: string, replacement: string, hashLen?: number, safeReapply?: boolean): string;
|
|
266
266
|
/**
|
|
267
267
|
* Apply a hash-aware edit operation directly against file content.
|
|
268
268
|
*
|
|
@@ -308,6 +308,11 @@ declare function matchesGlob(filePath: string, pattern: string): boolean;
|
|
|
308
308
|
* Check if a file path should be excluded based on config patterns.
|
|
309
309
|
*/
|
|
310
310
|
declare function shouldExclude(filePath: string, patterns: string[]): boolean;
|
|
311
|
+
/**
|
|
312
|
+
* Get the UTF-8 byte length of a string.
|
|
313
|
+
* Uses TextEncoder for accurate UTF-8 byte counting.
|
|
314
|
+
* This correctly handles multi-byte characters (Cyrillic, CJK, emoji, etc.).
|
|
315
|
+
*/
|
|
311
316
|
declare function getByteLength(content: string): number;
|
|
312
317
|
/**
|
|
313
318
|
* A Hashline instance with custom configuration.
|
|
@@ -154,23 +154,47 @@ 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 dirtyIndices = null;
|
|
163
|
+
let hasCollisions = true;
|
|
164
|
+
while (hasCollisions) {
|
|
165
|
+
hasCollisions = false;
|
|
166
|
+
const seen = /* @__PURE__ */ new Map();
|
|
167
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
168
|
+
const h = hashes[idx];
|
|
169
|
+
const group = seen.get(h);
|
|
170
|
+
if (group) {
|
|
171
|
+
group.push(idx);
|
|
172
|
+
} else {
|
|
173
|
+
seen.set(h, [idx]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const nextDirty = /* @__PURE__ */ new Set();
|
|
177
|
+
for (const [, group] of seen) {
|
|
178
|
+
if (group.length < 2) continue;
|
|
179
|
+
if (dirtyIndices !== null && !group.some((idx) => dirtyIndices.has(idx))) continue;
|
|
180
|
+
for (const idx of group) {
|
|
181
|
+
const newLen = Math.min(hashLens[idx] + 1, 8);
|
|
182
|
+
if (newLen === hashLens[idx]) continue;
|
|
183
|
+
hashLens[idx] = newLen;
|
|
184
|
+
hashes[idx] = computeLineHash(idx, lines[idx], newLen);
|
|
185
|
+
nextDirty.add(idx);
|
|
186
|
+
hasCollisions = true;
|
|
168
187
|
}
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
}
|
|
189
|
+
dirtyIndices = nextDirty;
|
|
190
|
+
}
|
|
191
|
+
const finalSeen = /* @__PURE__ */ new Map();
|
|
192
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
193
|
+
const existing = finalSeen.get(hashes[idx]);
|
|
194
|
+
if (existing !== void 0) {
|
|
195
|
+
hashes[idx] = `${hashes[idx]}${idx.toString(16)}`;
|
|
171
196
|
} else {
|
|
172
|
-
|
|
173
|
-
hashes[idx] = hash;
|
|
197
|
+
finalSeen.set(hashes[idx], idx);
|
|
174
198
|
}
|
|
175
199
|
}
|
|
176
200
|
const annotatedLines = lines.map((line, idx) => {
|
|
@@ -185,12 +209,16 @@ function formatFileWithHashes(content, hashLen, prefix, includeFileRev) {
|
|
|
185
209
|
function stripHashes(content, prefix) {
|
|
186
210
|
const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
|
|
187
211
|
const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
188
|
-
let
|
|
189
|
-
if (!
|
|
190
|
-
|
|
191
|
-
|
|
212
|
+
let cached = stripRegexCache.get(escapedPrefix);
|
|
213
|
+
if (!cached) {
|
|
214
|
+
cached = {
|
|
215
|
+
hashLine: new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`),
|
|
216
|
+
rev: new RegExp(`^${escapedPrefix}REV:[0-9a-f]{8}$`)
|
|
217
|
+
};
|
|
218
|
+
stripRegexCache.set(escapedPrefix, cached);
|
|
192
219
|
}
|
|
193
|
-
const
|
|
220
|
+
const hashLinePattern = cached.hashLine;
|
|
221
|
+
const revPattern = cached.rev;
|
|
194
222
|
const lineEnding = detectLineEnding(content);
|
|
195
223
|
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
196
224
|
const result = normalized.split("\n").filter((line) => !revPattern.test(line)).map((line) => {
|
|
@@ -339,10 +367,10 @@ function resolveRange(startRef, endRef, content, hashLen, safeReapply) {
|
|
|
339
367
|
content: rangeLines.join(lineEnding)
|
|
340
368
|
};
|
|
341
369
|
}
|
|
342
|
-
function replaceRange(startRef, endRef, content, replacement, hashLen) {
|
|
370
|
+
function replaceRange(startRef, endRef, content, replacement, hashLen, safeReapply) {
|
|
343
371
|
const lineEnding = detectLineEnding(content);
|
|
344
372
|
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
345
|
-
const range = resolveRange(startRef, endRef, normalized, hashLen);
|
|
373
|
+
const range = resolveRange(startRef, endRef, normalized, hashLen, safeReapply);
|
|
346
374
|
const lines = normalized.split("\n");
|
|
347
375
|
const before = lines.slice(0, range.startLine - 1);
|
|
348
376
|
const after = lines.slice(range.endLine);
|
|
@@ -453,7 +481,7 @@ function shouldExclude(filePath, patterns) {
|
|
|
453
481
|
return patterns.some((pattern) => matchesGlob(filePath, pattern));
|
|
454
482
|
}
|
|
455
483
|
function getByteLength(content) {
|
|
456
|
-
return
|
|
484
|
+
return Buffer.byteLength(content, "utf-8");
|
|
457
485
|
}
|
|
458
486
|
function detectLineEnding(content) {
|
|
459
487
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -521,7 +549,7 @@ function createHashline(config) {
|
|
|
521
549
|
}
|
|
522
550
|
};
|
|
523
551
|
}
|
|
524
|
-
var import_picomatch, DEFAULT_EXCLUDE_PATTERNS, DEFAULT_PREFIX, DEFAULT_CONFIG, HashlineError, modulusCache, stripRegexCache, HashlineCache, globMatcherCache
|
|
552
|
+
var import_picomatch, DEFAULT_EXCLUDE_PATTERNS, DEFAULT_PREFIX, DEFAULT_CONFIG, HashlineError, modulusCache, stripRegexCache, HashlineCache, globMatcherCache;
|
|
525
553
|
var init_hashline = __esm({
|
|
526
554
|
"src/hashline.ts"() {
|
|
527
555
|
"use strict";
|
|
@@ -692,7 +720,6 @@ var init_hashline = __esm({
|
|
|
692
720
|
}
|
|
693
721
|
};
|
|
694
722
|
globMatcherCache = /* @__PURE__ */ new Map();
|
|
695
|
-
textEncoder = new TextEncoder();
|
|
696
723
|
}
|
|
697
724
|
});
|
|
698
725
|
|
|
@@ -701,12 +728,14 @@ var src_exports = {};
|
|
|
701
728
|
__export(src_exports, {
|
|
702
729
|
HashlinePlugin: () => HashlinePlugin,
|
|
703
730
|
createHashlinePlugin: () => createHashlinePlugin,
|
|
704
|
-
default: () => src_default
|
|
731
|
+
default: () => src_default,
|
|
732
|
+
sanitizeConfig: () => sanitizeConfig
|
|
705
733
|
});
|
|
706
734
|
module.exports = __toCommonJS(src_exports);
|
|
707
735
|
var import_fs3 = require("fs");
|
|
708
736
|
var import_path3 = require("path");
|
|
709
737
|
var import_os2 = require("os");
|
|
738
|
+
var import_crypto = require("crypto");
|
|
710
739
|
var import_url = require("url");
|
|
711
740
|
|
|
712
741
|
// src/hooks.ts
|
|
@@ -716,18 +745,22 @@ var import_os = require("os");
|
|
|
716
745
|
init_hashline();
|
|
717
746
|
var DEBUG_LOG = (0, import_path.join)((0, import_os.homedir)(), ".config", "opencode", "hashline-debug.log");
|
|
718
747
|
var MAX_PROCESSED_IDS = 1e4;
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
748
|
+
var BoundedSet = class {
|
|
749
|
+
constructor(maxSize) {
|
|
750
|
+
this.maxSize = maxSize;
|
|
751
|
+
}
|
|
752
|
+
set = /* @__PURE__ */ new Set();
|
|
753
|
+
has(value) {
|
|
754
|
+
return this.set.has(value);
|
|
755
|
+
}
|
|
756
|
+
add(value) {
|
|
757
|
+
if (this.set.size >= this.maxSize) {
|
|
758
|
+
const first = this.set.values().next().value;
|
|
759
|
+
if (first !== void 0) this.set.delete(first);
|
|
726
760
|
}
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
}
|
|
761
|
+
this.set.add(value);
|
|
762
|
+
}
|
|
763
|
+
};
|
|
731
764
|
var debugEnabled = false;
|
|
732
765
|
function setDebug(enabled) {
|
|
733
766
|
debugEnabled = enabled;
|
|
@@ -762,7 +795,7 @@ function createFileReadAfterHook(cache, config) {
|
|
|
762
795
|
const resolved = config ?? resolveConfig();
|
|
763
796
|
const hashLen = resolved.hashLength || 0;
|
|
764
797
|
const prefix = resolved.prefix;
|
|
765
|
-
const processedCallIds =
|
|
798
|
+
const processedCallIds = new BoundedSet(MAX_PROCESSED_IDS);
|
|
766
799
|
return async (input, output) => {
|
|
767
800
|
debug("tool.execute.after:", input.tool, "args:", input.args);
|
|
768
801
|
if (input.callID) {
|
|
@@ -771,6 +804,8 @@ function createFileReadAfterHook(cache, config) {
|
|
|
771
804
|
return;
|
|
772
805
|
}
|
|
773
806
|
processedCallIds.add(input.callID);
|
|
807
|
+
} else {
|
|
808
|
+
debug("no callID \u2014 deduplication disabled for this call");
|
|
774
809
|
}
|
|
775
810
|
if (!isFileReadTool(input.tool, input.args)) {
|
|
776
811
|
debug("skipped: not a file-read tool");
|
|
@@ -809,13 +844,16 @@ function createFileReadAfterHook(cache, config) {
|
|
|
809
844
|
function createFileEditBeforeHook(config) {
|
|
810
845
|
const resolved = config ?? resolveConfig();
|
|
811
846
|
const prefix = resolved.prefix;
|
|
812
|
-
const processedCallIds =
|
|
847
|
+
const processedCallIds = new BoundedSet(MAX_PROCESSED_IDS);
|
|
813
848
|
return async (input, output) => {
|
|
814
849
|
if (input.callID) {
|
|
815
850
|
if (processedCallIds.has(input.callID)) {
|
|
851
|
+
debug("skipped: duplicate callID (edit)", input.callID);
|
|
816
852
|
return;
|
|
817
853
|
}
|
|
818
854
|
processedCallIds.add(input.callID);
|
|
855
|
+
} else {
|
|
856
|
+
debug("no callID \u2014 deduplication disabled for this edit call");
|
|
819
857
|
}
|
|
820
858
|
const toolName = input.tool.toLowerCase();
|
|
821
859
|
const isFileEdit = FILE_EDIT_TOOLS.some(
|
|
@@ -1067,14 +1105,39 @@ ${error.toDiagnostic()}`);
|
|
|
1067
1105
|
|
|
1068
1106
|
// src/index.ts
|
|
1069
1107
|
var CONFIG_FILENAME = "opencode-hashline.json";
|
|
1108
|
+
var tempDirs = /* @__PURE__ */ new Set();
|
|
1109
|
+
var exitListenerRegistered = false;
|
|
1110
|
+
function registerTempDir(dir) {
|
|
1111
|
+
tempDirs.add(dir);
|
|
1112
|
+
if (!exitListenerRegistered) {
|
|
1113
|
+
exitListenerRegistered = true;
|
|
1114
|
+
process.on("exit", () => {
|
|
1115
|
+
for (const d of tempDirs) {
|
|
1116
|
+
try {
|
|
1117
|
+
(0, import_fs3.rmSync)(d, { recursive: true, force: true });
|
|
1118
|
+
} catch {
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
function writeTempFile(tempDir, content) {
|
|
1125
|
+
const name = `hl-${(0, import_crypto.randomBytes)(16).toString("hex")}.txt`;
|
|
1126
|
+
const tmpPath = (0, import_path3.join)(tempDir, name);
|
|
1127
|
+
const fd = (0, import_fs3.openSync)(tmpPath, import_fs3.constants.O_WRONLY | import_fs3.constants.O_CREAT | import_fs3.constants.O_EXCL, 384);
|
|
1128
|
+
try {
|
|
1129
|
+
(0, import_fs3.writeFileSync)(fd, content, "utf-8");
|
|
1130
|
+
} finally {
|
|
1131
|
+
(0, import_fs3.closeSync)(fd);
|
|
1132
|
+
}
|
|
1133
|
+
return tmpPath;
|
|
1134
|
+
}
|
|
1070
1135
|
function sanitizeConfig(raw) {
|
|
1071
1136
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {};
|
|
1072
1137
|
const r = raw;
|
|
1073
1138
|
const result = {};
|
|
1074
1139
|
if (Array.isArray(r.exclude)) {
|
|
1075
|
-
result.exclude = r.exclude.filter(
|
|
1076
|
-
(p) => typeof p === "string" && p.length <= 512
|
|
1077
|
-
);
|
|
1140
|
+
result.exclude = r.exclude.filter((p) => typeof p === "string" && p.length <= 512).slice(0, 1e3);
|
|
1078
1141
|
}
|
|
1079
1142
|
if (typeof r.maxFileSize === "number" && Number.isFinite(r.maxFileSize) && r.maxFileSize >= 0) {
|
|
1080
1143
|
result.maxFileSize = r.maxFileSize;
|
|
@@ -1126,14 +1189,13 @@ function loadConfig(projectDir, userConfig) {
|
|
|
1126
1189
|
}
|
|
1127
1190
|
function createHashlinePlugin(userConfig) {
|
|
1128
1191
|
return async (input) => {
|
|
1129
|
-
const projectDir = input
|
|
1130
|
-
const worktree = input.worktree;
|
|
1192
|
+
const { directory: projectDir, worktree } = input;
|
|
1131
1193
|
const fileConfig = loadConfig(projectDir, userConfig);
|
|
1132
1194
|
const config = resolveConfig(fileConfig);
|
|
1133
1195
|
const cache = new HashlineCache(config.cacheSize);
|
|
1134
1196
|
setDebug(config.debug);
|
|
1135
|
-
const { appendFileSync: writeLog } = await import("fs");
|
|
1136
1197
|
const debugLog = (0, import_path3.join)((0, import_os2.homedir)(), ".config", "opencode", "hashline-debug.log");
|
|
1198
|
+
const writeLog = import_fs3.appendFileSync;
|
|
1137
1199
|
if (config.debug) {
|
|
1138
1200
|
try {
|
|
1139
1201
|
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] plugin loaded, prefix: ${JSON.stringify(config.prefix)}, maxFileSize: ${config.maxFileSize}, projectDir: ${projectDir}
|
|
@@ -1141,17 +1203,8 @@ function createHashlinePlugin(userConfig) {
|
|
|
1141
1203
|
} catch {
|
|
1142
1204
|
}
|
|
1143
1205
|
}
|
|
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);
|
|
1206
|
+
const instanceTmpDir = (0, import_fs3.mkdtempSync)((0, import_path3.join)((0, import_os2.tmpdir)(), "hashline-"));
|
|
1207
|
+
registerTempDir(instanceTmpDir);
|
|
1155
1208
|
return {
|
|
1156
1209
|
tool: {
|
|
1157
1210
|
hashline_edit: createHashlineEditTool(config, cache)
|
|
@@ -1194,9 +1247,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
1194
1247
|
if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
|
|
1195
1248
|
const cached = cache.get(filePath, content);
|
|
1196
1249
|
if (cached) {
|
|
1197
|
-
const tmpPath2 = (
|
|
1198
|
-
(0, import_fs3.writeFileSync)(tmpPath2, cached, "utf-8");
|
|
1199
|
-
tempFiles.add(tmpPath2);
|
|
1250
|
+
const tmpPath2 = writeTempFile(instanceTmpDir, cached);
|
|
1200
1251
|
p.url = `file://${tmpPath2}`;
|
|
1201
1252
|
if (config.debug) {
|
|
1202
1253
|
try {
|
|
@@ -1207,10 +1258,9 @@ function createHashlinePlugin(userConfig) {
|
|
|
1207
1258
|
}
|
|
1208
1259
|
continue;
|
|
1209
1260
|
}
|
|
1210
|
-
const annotated = formatFileWithHashes2(content, hashLen || void 0, prefix);
|
|
1261
|
+
const annotated = formatFileWithHashes2(content, hashLen || void 0, prefix, config.fileRev);
|
|
1211
1262
|
cache.set(filePath, content, annotated);
|
|
1212
|
-
const tmpPath = (
|
|
1213
|
-
(0, import_fs3.writeFileSync)(tmpPath, annotated, "utf-8");
|
|
1263
|
+
const tmpPath = writeTempFile(instanceTmpDir, annotated);
|
|
1214
1264
|
p.url = `file://${tmpPath}`;
|
|
1215
1265
|
if (config.debug) {
|
|
1216
1266
|
try {
|
|
@@ -1238,5 +1288,6 @@ var src_default = HashlinePlugin;
|
|
|
1238
1288
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1239
1289
|
0 && (module.exports = {
|
|
1240
1290
|
HashlinePlugin,
|
|
1241
|
-
createHashlinePlugin
|
|
1291
|
+
createHashlinePlugin,
|
|
1292
|
+
sanitizeConfig
|
|
1242
1293
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
-
import { H as HashlineConfig } from './hashline-
|
|
3
|
-
export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-
|
|
2
|
+
import { H as HashlineConfig } from './hashline-DWndArr4.cjs';
|
|
3
|
+
export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-DWndArr4.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* opencode-hashline — Hashline plugin for OpenCode
|
|
@@ -14,6 +14,13 @@ export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as Ha
|
|
|
14
14
|
* constants, import from "opencode-hashline/utils".
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize and validate a raw parsed config object.
|
|
19
|
+
* Accepts only known keys with expected types; silently drops invalid values.
|
|
20
|
+
* This prevents prototype pollution, type confusion, and prompt injection via
|
|
21
|
+
* a malicious or hand-crafted config file.
|
|
22
|
+
*/
|
|
23
|
+
declare function sanitizeConfig(raw: unknown): HashlineConfig;
|
|
17
24
|
/**
|
|
18
25
|
* Create a Hashline plugin instance with optional user configuration.
|
|
19
26
|
*
|
|
@@ -45,4 +52,4 @@ declare function createHashlinePlugin(userConfig?: HashlineConfig): Plugin;
|
|
|
45
52
|
*/
|
|
46
53
|
declare const HashlinePlugin: Plugin;
|
|
47
54
|
|
|
48
|
-
export { HashlineConfig, HashlinePlugin, createHashlinePlugin, HashlinePlugin as default };
|
|
55
|
+
export { HashlineConfig, HashlinePlugin, createHashlinePlugin, HashlinePlugin as default, sanitizeConfig };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from '@opencode-ai/plugin';
|
|
2
|
-
import { H as HashlineConfig } from './hashline-
|
|
3
|
-
export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-
|
|
2
|
+
import { H as HashlineConfig } from './hashline-DWndArr4.js';
|
|
3
|
+
export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as HashEditResult, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult } from './hashline-DWndArr4.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* opencode-hashline — Hashline plugin for OpenCode
|
|
@@ -14,6 +14,13 @@ export { C as CandidateLine, a as HashEditInput, b as HashEditOperation, c as Ha
|
|
|
14
14
|
* constants, import from "opencode-hashline/utils".
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sanitize and validate a raw parsed config object.
|
|
19
|
+
* Accepts only known keys with expected types; silently drops invalid values.
|
|
20
|
+
* This prevents prototype pollution, type confusion, and prompt injection via
|
|
21
|
+
* a malicious or hand-crafted config file.
|
|
22
|
+
*/
|
|
23
|
+
declare function sanitizeConfig(raw: unknown): HashlineConfig;
|
|
17
24
|
/**
|
|
18
25
|
* Create a Hashline plugin instance with optional user configuration.
|
|
19
26
|
*
|
|
@@ -45,4 +52,4 @@ declare function createHashlinePlugin(userConfig?: HashlineConfig): Plugin;
|
|
|
45
52
|
*/
|
|
46
53
|
declare const HashlinePlugin: Plugin;
|
|
47
54
|
|
|
48
|
-
export { HashlineConfig, HashlinePlugin, createHashlinePlugin, HashlinePlugin as default };
|
|
55
|
+
export { HashlineConfig, HashlinePlugin, createHashlinePlugin, HashlinePlugin as default, sanitizeConfig };
|
|
@@ -3,19 +3,20 @@ import {
|
|
|
3
3
|
createFileReadAfterHook,
|
|
4
4
|
createSystemPromptHook,
|
|
5
5
|
setDebug
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-3ED7MDEC.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-C323JLG3.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, appendFileSync, 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,14 +141,39 @@ ${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;
|
|
146
174
|
const result = {};
|
|
147
175
|
if (Array.isArray(r.exclude)) {
|
|
148
|
-
result.exclude = r.exclude.filter(
|
|
149
|
-
(p) => typeof p === "string" && p.length <= 512
|
|
150
|
-
);
|
|
176
|
+
result.exclude = r.exclude.filter((p) => typeof p === "string" && p.length <= 512).slice(0, 1e3);
|
|
151
177
|
}
|
|
152
178
|
if (typeof r.maxFileSize === "number" && Number.isFinite(r.maxFileSize) && r.maxFileSize >= 0) {
|
|
153
179
|
result.maxFileSize = r.maxFileSize;
|
|
@@ -199,14 +225,13 @@ function loadConfig(projectDir, userConfig) {
|
|
|
199
225
|
}
|
|
200
226
|
function createHashlinePlugin(userConfig) {
|
|
201
227
|
return async (input) => {
|
|
202
|
-
const projectDir = input
|
|
203
|
-
const worktree = input.worktree;
|
|
228
|
+
const { directory: projectDir, worktree } = input;
|
|
204
229
|
const fileConfig = loadConfig(projectDir, userConfig);
|
|
205
230
|
const config = resolveConfig(fileConfig);
|
|
206
231
|
const cache = new HashlineCache(config.cacheSize);
|
|
207
232
|
setDebug(config.debug);
|
|
208
|
-
const { appendFileSync: writeLog } = await import("fs");
|
|
209
233
|
const debugLog = join(homedir(), ".config", "opencode", "hashline-debug.log");
|
|
234
|
+
const writeLog = appendFileSync;
|
|
210
235
|
if (config.debug) {
|
|
211
236
|
try {
|
|
212
237
|
writeLog(debugLog, `[${(/* @__PURE__ */ new Date()).toISOString()}] plugin loaded, prefix: ${JSON.stringify(config.prefix)}, maxFileSize: ${config.maxFileSize}, projectDir: ${projectDir}
|
|
@@ -214,17 +239,8 @@ function createHashlinePlugin(userConfig) {
|
|
|
214
239
|
} catch {
|
|
215
240
|
}
|
|
216
241
|
}
|
|
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);
|
|
242
|
+
const instanceTmpDir = mkdtempSync(join(tmpdir(), "hashline-"));
|
|
243
|
+
registerTempDir(instanceTmpDir);
|
|
228
244
|
return {
|
|
229
245
|
tool: {
|
|
230
246
|
hashline_edit: createHashlineEditTool(config, cache)
|
|
@@ -237,7 +253,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
237
253
|
const out = output;
|
|
238
254
|
const hashLen = config.hashLength || 0;
|
|
239
255
|
const prefix = config.prefix;
|
|
240
|
-
const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-
|
|
256
|
+
const { formatFileWithHashes, shouldExclude, getByteLength: getByteLength2 } = await import("./hashline-DDPVX355.js");
|
|
241
257
|
for (const p of out.parts ?? []) {
|
|
242
258
|
if (p.type !== "file") continue;
|
|
243
259
|
if (!p.url || !p.mime?.startsWith("text/")) continue;
|
|
@@ -267,9 +283,7 @@ function createHashlinePlugin(userConfig) {
|
|
|
267
283
|
if (config.maxFileSize > 0 && getByteLength2(content) > config.maxFileSize) continue;
|
|
268
284
|
const cached = cache.get(filePath, content);
|
|
269
285
|
if (cached) {
|
|
270
|
-
const tmpPath2 =
|
|
271
|
-
writeFileSync2(tmpPath2, cached, "utf-8");
|
|
272
|
-
tempFiles.add(tmpPath2);
|
|
286
|
+
const tmpPath2 = writeTempFile(instanceTmpDir, cached);
|
|
273
287
|
p.url = `file://${tmpPath2}`;
|
|
274
288
|
if (config.debug) {
|
|
275
289
|
try {
|
|
@@ -280,10 +294,9 @@ function createHashlinePlugin(userConfig) {
|
|
|
280
294
|
}
|
|
281
295
|
continue;
|
|
282
296
|
}
|
|
283
|
-
const annotated = formatFileWithHashes(content, hashLen || void 0, prefix);
|
|
297
|
+
const annotated = formatFileWithHashes(content, hashLen || void 0, prefix, config.fileRev);
|
|
284
298
|
cache.set(filePath, content, annotated);
|
|
285
|
-
const tmpPath =
|
|
286
|
-
writeFileSync2(tmpPath, annotated, "utf-8");
|
|
299
|
+
const tmpPath = writeTempFile(instanceTmpDir, annotated);
|
|
287
300
|
p.url = `file://${tmpPath}`;
|
|
288
301
|
if (config.debug) {
|
|
289
302
|
try {
|
|
@@ -311,5 +324,6 @@ var src_default = HashlinePlugin;
|
|
|
311
324
|
export {
|
|
312
325
|
HashlinePlugin,
|
|
313
326
|
createHashlinePlugin,
|
|
314
|
-
src_default as default
|
|
327
|
+
src_default as default,
|
|
328
|
+
sanitizeConfig
|
|
315
329
|
};
|
package/dist/utils.cjs
CHANGED
|
@@ -263,23 +263,47 @@ 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 dirtyIndices = null;
|
|
272
|
+
let hasCollisions = true;
|
|
273
|
+
while (hasCollisions) {
|
|
274
|
+
hasCollisions = false;
|
|
275
|
+
const seen = /* @__PURE__ */ new Map();
|
|
276
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
277
|
+
const h = hashes[idx];
|
|
278
|
+
const group = seen.get(h);
|
|
279
|
+
if (group) {
|
|
280
|
+
group.push(idx);
|
|
281
|
+
} else {
|
|
282
|
+
seen.set(h, [idx]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const nextDirty = /* @__PURE__ */ new Set();
|
|
286
|
+
for (const [, group] of seen) {
|
|
287
|
+
if (group.length < 2) continue;
|
|
288
|
+
if (dirtyIndices !== null && !group.some((idx) => dirtyIndices.has(idx))) continue;
|
|
289
|
+
for (const idx of group) {
|
|
290
|
+
const newLen = Math.min(hashLens[idx] + 1, 8);
|
|
291
|
+
if (newLen === hashLens[idx]) continue;
|
|
292
|
+
hashLens[idx] = newLen;
|
|
293
|
+
hashes[idx] = computeLineHash(idx, lines[idx], newLen);
|
|
294
|
+
nextDirty.add(idx);
|
|
295
|
+
hasCollisions = true;
|
|
277
296
|
}
|
|
278
|
-
|
|
279
|
-
|
|
297
|
+
}
|
|
298
|
+
dirtyIndices = nextDirty;
|
|
299
|
+
}
|
|
300
|
+
const finalSeen = /* @__PURE__ */ new Map();
|
|
301
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
302
|
+
const existing = finalSeen.get(hashes[idx]);
|
|
303
|
+
if (existing !== void 0) {
|
|
304
|
+
hashes[idx] = `${hashes[idx]}${idx.toString(16)}`;
|
|
280
305
|
} else {
|
|
281
|
-
|
|
282
|
-
hashes[idx] = hash;
|
|
306
|
+
finalSeen.set(hashes[idx], idx);
|
|
283
307
|
}
|
|
284
308
|
}
|
|
285
309
|
const annotatedLines = lines.map((line, idx) => {
|
|
@@ -295,12 +319,16 @@ var stripRegexCache = /* @__PURE__ */ new Map();
|
|
|
295
319
|
function stripHashes(content, prefix) {
|
|
296
320
|
const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
|
|
297
321
|
const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
298
|
-
let
|
|
299
|
-
if (!
|
|
300
|
-
|
|
301
|
-
|
|
322
|
+
let cached = stripRegexCache.get(escapedPrefix);
|
|
323
|
+
if (!cached) {
|
|
324
|
+
cached = {
|
|
325
|
+
hashLine: new RegExp(`^([+ \\-])?${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`),
|
|
326
|
+
rev: new RegExp(`^${escapedPrefix}REV:[0-9a-f]{8}$`)
|
|
327
|
+
};
|
|
328
|
+
stripRegexCache.set(escapedPrefix, cached);
|
|
302
329
|
}
|
|
303
|
-
const
|
|
330
|
+
const hashLinePattern = cached.hashLine;
|
|
331
|
+
const revPattern = cached.rev;
|
|
304
332
|
const lineEnding = detectLineEnding(content);
|
|
305
333
|
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
306
334
|
const result = normalized.split("\n").filter((line) => !revPattern.test(line)).map((line) => {
|
|
@@ -449,10 +477,10 @@ function resolveRange(startRef, endRef, content, hashLen, safeReapply) {
|
|
|
449
477
|
content: rangeLines.join(lineEnding)
|
|
450
478
|
};
|
|
451
479
|
}
|
|
452
|
-
function replaceRange(startRef, endRef, content, replacement, hashLen) {
|
|
480
|
+
function replaceRange(startRef, endRef, content, replacement, hashLen, safeReapply) {
|
|
453
481
|
const lineEnding = detectLineEnding(content);
|
|
454
482
|
const normalized = lineEnding === "\r\n" ? content.replace(/\r\n/g, "\n") : content;
|
|
455
|
-
const range = resolveRange(startRef, endRef, normalized, hashLen);
|
|
483
|
+
const range = resolveRange(startRef, endRef, normalized, hashLen, safeReapply);
|
|
456
484
|
const lines = normalized.split("\n");
|
|
457
485
|
const before = lines.slice(0, range.startLine - 1);
|
|
458
486
|
const after = lines.slice(range.endLine);
|
|
@@ -622,9 +650,8 @@ function matchesGlob(filePath, pattern) {
|
|
|
622
650
|
function shouldExclude(filePath, patterns) {
|
|
623
651
|
return patterns.some((pattern) => matchesGlob(filePath, pattern));
|
|
624
652
|
}
|
|
625
|
-
var textEncoder = new TextEncoder();
|
|
626
653
|
function getByteLength(content) {
|
|
627
|
-
return
|
|
654
|
+
return Buffer.byteLength(content, "utf-8");
|
|
628
655
|
}
|
|
629
656
|
function detectLineEnding(content) {
|
|
630
657
|
return content.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -699,18 +726,22 @@ var import_path = require("path");
|
|
|
699
726
|
var import_os = require("os");
|
|
700
727
|
var DEBUG_LOG = (0, import_path.join)((0, import_os.homedir)(), ".config", "opencode", "hashline-debug.log");
|
|
701
728
|
var MAX_PROCESSED_IDS = 1e4;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
729
|
+
var BoundedSet = class {
|
|
730
|
+
constructor(maxSize) {
|
|
731
|
+
this.maxSize = maxSize;
|
|
732
|
+
}
|
|
733
|
+
set = /* @__PURE__ */ new Set();
|
|
734
|
+
has(value) {
|
|
735
|
+
return this.set.has(value);
|
|
736
|
+
}
|
|
737
|
+
add(value) {
|
|
738
|
+
if (this.set.size >= this.maxSize) {
|
|
739
|
+
const first = this.set.values().next().value;
|
|
740
|
+
if (first !== void 0) this.set.delete(first);
|
|
709
741
|
}
|
|
710
|
-
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
}
|
|
742
|
+
this.set.add(value);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
714
745
|
var debugEnabled = false;
|
|
715
746
|
function debug(...args) {
|
|
716
747
|
if (!debugEnabled) return;
|
|
@@ -742,7 +773,7 @@ function createFileReadAfterHook(cache, config) {
|
|
|
742
773
|
const resolved = config ?? resolveConfig();
|
|
743
774
|
const hashLen = resolved.hashLength || 0;
|
|
744
775
|
const prefix = resolved.prefix;
|
|
745
|
-
const processedCallIds =
|
|
776
|
+
const processedCallIds = new BoundedSet(MAX_PROCESSED_IDS);
|
|
746
777
|
return async (input, output) => {
|
|
747
778
|
debug("tool.execute.after:", input.tool, "args:", input.args);
|
|
748
779
|
if (input.callID) {
|
|
@@ -751,6 +782,8 @@ function createFileReadAfterHook(cache, config) {
|
|
|
751
782
|
return;
|
|
752
783
|
}
|
|
753
784
|
processedCallIds.add(input.callID);
|
|
785
|
+
} else {
|
|
786
|
+
debug("no callID \u2014 deduplication disabled for this call");
|
|
754
787
|
}
|
|
755
788
|
if (!isFileReadTool(input.tool, input.args)) {
|
|
756
789
|
debug("skipped: not a file-read tool");
|
|
@@ -789,13 +822,16 @@ function createFileReadAfterHook(cache, config) {
|
|
|
789
822
|
function createFileEditBeforeHook(config) {
|
|
790
823
|
const resolved = config ?? resolveConfig();
|
|
791
824
|
const prefix = resolved.prefix;
|
|
792
|
-
const processedCallIds =
|
|
825
|
+
const processedCallIds = new BoundedSet(MAX_PROCESSED_IDS);
|
|
793
826
|
return async (input, output) => {
|
|
794
827
|
if (input.callID) {
|
|
795
828
|
if (processedCallIds.has(input.callID)) {
|
|
829
|
+
debug("skipped: duplicate callID (edit)", input.callID);
|
|
796
830
|
return;
|
|
797
831
|
}
|
|
798
832
|
processedCallIds.add(input.callID);
|
|
833
|
+
} else {
|
|
834
|
+
debug("no callID \u2014 deduplication disabled for this edit call");
|
|
799
835
|
}
|
|
800
836
|
const toolName = input.tool.toLowerCase();
|
|
801
837
|
const isFileEdit = FILE_EDIT_TOOLS.some(
|
package/dist/utils.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HashlineConfig, f as HashlineCache } from './hashline-
|
|
2
|
-
export { C as CandidateLine, D as DEFAULT_CONFIG, g as DEFAULT_EXCLUDE_PATTERNS, h as DEFAULT_PREFIX, a as HashEditInput, b as HashEditOperation, c as HashEditResult, i as HashlineError, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult, j as applyHashEdit, k as buildHashMap, l as computeFileRev, m as computeLineHash, n as createHashline, o as extractFileRev, p as findCandidateLines, q as formatFileWithHashes, r as getAdaptiveHashLength, s as getByteLength, t as matchesGlob, u as normalizeHashRef, v as parseHashRef, w as replaceRange, x as resolveConfig, y as resolveRange, z as shouldExclude, A as stripHashes, B as verifyFileRev, E as verifyHash } from './hashline-
|
|
1
|
+
import { H as HashlineConfig, f as HashlineCache } from './hashline-DWndArr4.cjs';
|
|
2
|
+
export { C as CandidateLine, D as DEFAULT_CONFIG, g as DEFAULT_EXCLUDE_PATTERNS, h as DEFAULT_PREFIX, a as HashEditInput, b as HashEditOperation, c as HashEditResult, i as HashlineError, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult, j as applyHashEdit, k as buildHashMap, l as computeFileRev, m as computeLineHash, n as createHashline, o as extractFileRev, p as findCandidateLines, q as formatFileWithHashes, r as getAdaptiveHashLength, s as getByteLength, t as matchesGlob, u as normalizeHashRef, v as parseHashRef, w as replaceRange, x as resolveConfig, y as resolveRange, z as shouldExclude, A as stripHashes, B as verifyFileRev, E as verifyHash } from './hashline-DWndArr4.cjs';
|
|
3
3
|
import { Hooks } from '@opencode-ai/plugin';
|
|
4
4
|
|
|
5
5
|
/**
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HashlineConfig, f as HashlineCache } from './hashline-
|
|
2
|
-
export { C as CandidateLine, D as DEFAULT_CONFIG, g as DEFAULT_EXCLUDE_PATTERNS, h as DEFAULT_PREFIX, a as HashEditInput, b as HashEditOperation, c as HashEditResult, i as HashlineError, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult, j as applyHashEdit, k as buildHashMap, l as computeFileRev, m as computeLineHash, n as createHashline, o as extractFileRev, p as findCandidateLines, q as formatFileWithHashes, r as getAdaptiveHashLength, s as getByteLength, t as matchesGlob, u as normalizeHashRef, v as parseHashRef, w as replaceRange, x as resolveConfig, y as resolveRange, z as shouldExclude, A as stripHashes, B as verifyFileRev, E as verifyHash } from './hashline-
|
|
1
|
+
import { H as HashlineConfig, f as HashlineCache } from './hashline-DWndArr4.js';
|
|
2
|
+
export { C as CandidateLine, D as DEFAULT_CONFIG, g as DEFAULT_EXCLUDE_PATTERNS, h as DEFAULT_PREFIX, a as HashEditInput, b as HashEditOperation, c as HashEditResult, i as HashlineError, d as HashlineErrorCode, e as HashlineInstance, R as ResolvedRange, V as VerifyHashResult, j as applyHashEdit, k as buildHashMap, l as computeFileRev, m as computeLineHash, n as createHashline, o as extractFileRev, p as findCandidateLines, q as formatFileWithHashes, r as getAdaptiveHashLength, s as getByteLength, t as matchesGlob, u as normalizeHashRef, v as parseHashRef, w as replaceRange, x as resolveConfig, y as resolveRange, z as shouldExclude, A as stripHashes, B as verifyFileRev, E as verifyHash } from './hashline-DWndArr4.js';
|
|
3
3
|
import { Hooks } from '@opencode-ai/plugin';
|
|
4
4
|
|
|
5
5
|
/**
|
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-3ED7MDEC.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-C323JLG3.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.2",
|
|
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",
|