opencode-hashline 1.2.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 +55 -6
- package/README.md +55 -6
- package/README.ru.md +7 -6
- package/dist/{chunk-I6RACR3D.js → chunk-GKXY5ZBM.js} +266 -57
- package/dist/{chunk-VPCMHCTB.js → chunk-VSVVWPET.js} +25 -3
- package/dist/{hashline-5PFAXY3H.js → hashline-37RYBX5A.js} +11 -1
- package/dist/{hashline-yhMw1Abs.d.ts → hashline-A7k2yn3G.d.cts} +70 -5
- package/dist/{hashline-yhMw1Abs.d.cts → hashline-A7k2yn3G.d.ts} +70 -5
- package/dist/opencode-hashline.cjs +342 -81
- package/dist/opencode-hashline.d.cts +2 -2
- package/dist/opencode-hashline.d.ts +2 -2
- package/dist/opencode-hashline.js +56 -25
- package/dist/utils.cjs +295 -59
- package/dist/utils.d.cts +2 -2
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +12 -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) {
|
|
@@ -104,6 +105,49 @@ if (!result.valid) {
|
|
|
104
105
|
|
|
105
106
|
Hash verification uses the length of the provided hash reference (not the current file size), so a reference like `2:f1` remains valid even if the file has grown.
|
|
106
107
|
|
|
108
|
+
### 🔒 File Revision (`fileRev`)
|
|
109
|
+
|
|
110
|
+
In addition to per-line hashes, hashline computes a whole-file hash (FNV-1a, 8 hex chars). It's prepended as the first annotation line:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
#HL REV:72c4946c
|
|
114
|
+
#HL 1:a3f|function hello() {
|
|
115
|
+
#HL 2:f1c| return "world";
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Pass `fileRev` to `hashline_edit` when editing — if the file changed since it was read, the edit is rejected with `FILE_REV_MISMATCH`.
|
|
119
|
+
|
|
120
|
+
### 🔄 Safe Reapply
|
|
121
|
+
|
|
122
|
+
If a line moved (e.g., due to insertions above), `safeReapply` finds it by content hash:
|
|
123
|
+
|
|
124
|
+
- **1 candidate** — edit applies at the new position
|
|
125
|
+
- **>1 candidates** — `AMBIGUOUS_REAPPLY` error (ambiguous)
|
|
126
|
+
- **0 candidates** — `HASH_MISMATCH` error
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const result = applyHashEdit(
|
|
130
|
+
{ operation: "replace", startRef: "1:a3f", replacement: "new" },
|
|
131
|
+
content,
|
|
132
|
+
undefined,
|
|
133
|
+
true, // safeReapply
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 🏷️ Structured Errors
|
|
138
|
+
|
|
139
|
+
All hashline errors are instances of `HashlineError` with error codes, diagnostics, and hints:
|
|
140
|
+
|
|
141
|
+
| Code | Description |
|
|
142
|
+
|------|-------------|
|
|
143
|
+
| `HASH_MISMATCH` | Line content changed since last read |
|
|
144
|
+
| `FILE_REV_MISMATCH` | File was modified since last read |
|
|
145
|
+
| `AMBIGUOUS_REAPPLY` | Multiple candidates found during safe reapply |
|
|
146
|
+
| `TARGET_OUT_OF_RANGE` | Line number exceeds file length |
|
|
147
|
+
| `INVALID_REF` | Malformed hash reference |
|
|
148
|
+
| `INVALID_RANGE` | Start line is after end line |
|
|
149
|
+
| `MISSING_REPLACEMENT` | Replace/insert operation without content |
|
|
150
|
+
|
|
107
151
|
### 🔍 Indentation-Sensitive Hashing
|
|
108
152
|
|
|
109
153
|
Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespace (indentation) are detected as content changes, while trailing whitespace is ignored.
|
|
@@ -113,7 +157,7 @@ Hash computation uses `trimEnd()` (not `trim()`), so changes to leading whitespa
|
|
|
113
157
|
Resolve and replace ranges of lines by hash references:
|
|
114
158
|
|
|
115
159
|
```typescript
|
|
116
|
-
import { resolveRange, replaceRange } from "opencode-hashline";
|
|
160
|
+
import { resolveRange, replaceRange } from "opencode-hashline/utils";
|
|
117
161
|
|
|
118
162
|
// Get lines between two hash references
|
|
119
163
|
const range = resolveRange("1:a3f", "3:0e7", content);
|
|
@@ -131,7 +175,7 @@ const newContent = replaceRange(
|
|
|
131
175
|
Create custom Hashline instances with specific settings:
|
|
132
176
|
|
|
133
177
|
```typescript
|
|
134
|
-
import { createHashline } from "opencode-hashline";
|
|
178
|
+
import { createHashline } from "opencode-hashline/utils";
|
|
135
179
|
|
|
136
180
|
const hl = createHashline({
|
|
137
181
|
exclude: ["**/node_modules/**", "**/*.min.js"],
|
|
@@ -151,10 +195,12 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
|
|
|
151
195
|
| Option | Type | Default | Description |
|
|
152
196
|
|--------|------|---------|-------------|
|
|
153
197
|
| `exclude` | `string[]` | See below | Glob patterns for files to skip |
|
|
154
|
-
| `maxFileSize` | `number` | `
|
|
198
|
+
| `maxFileSize` | `number` | `1_048_576` (1 MB) | Max file size in bytes |
|
|
155
199
|
| `hashLength` | `number \| undefined` | `undefined` (adaptive) | Force specific hash length |
|
|
156
200
|
| `cacheSize` | `number` | `100` | Max files in LRU cache |
|
|
157
201
|
| `prefix` | `string \| false` | `"#HL "` | Line prefix (`false` to disable) |
|
|
202
|
+
| `fileRev` | `boolean` | `true` | Include file revision hash (`#HL REV:...`) in annotations |
|
|
203
|
+
| `safeReapply` | `boolean` | `false` | Auto-relocate moved lines by content hash |
|
|
158
204
|
|
|
159
205
|
Default exclude patterns cover: lock files, `node_modules`, minified files, binary files (images, fonts, archives, etc.).
|
|
160
206
|
|
|
@@ -257,7 +303,7 @@ The plugin needs to determine which tools are "file-read" tools (to annotate the
|
|
|
257
303
|
The `isFileReadTool()` function is exported for testing and advanced usage:
|
|
258
304
|
|
|
259
305
|
```typescript
|
|
260
|
-
import { isFileReadTool } from "opencode-hashline";
|
|
306
|
+
import { isFileReadTool } from "opencode-hashline/utils";
|
|
261
307
|
|
|
262
308
|
isFileReadTool("read_file"); // true
|
|
263
309
|
isFileReadTool("mcp.read"); // true
|
|
@@ -442,9 +488,12 @@ The idea behind hashline is inspired by concepts from **oh-my-pi** by [can1357](
|
|
|
442
488
|
|
|
443
489
|
Hashline solves this by assigning each line a short, deterministic hash tag (e.g. `2:f1c`), making line addressing **exact and unambiguous**. The model can reference any line or range precisely, eliminating off-by-one errors and duplicate-line confusion.
|
|
444
490
|
|
|
491
|
+
The advanced features — **file revision** (`fileRev`), **safe reapply**, and **structured errors** — are inspired by the hash-based editing implementation in **AssistAgents** by [OzeroHAX](https://github.com/OzeroHAX/AssistAgents), which independently applied a similar approach for OpenCode with additional integrity checks and error diagnostics.
|
|
492
|
+
|
|
445
493
|
**References:**
|
|
446
494
|
- [oh-my-pi by can1357](https://github.com/can1357/oh-my-pi) — AI coding agent toolkit: coding agent CLI, unified LLM API, TUI libraries
|
|
447
495
|
- [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — blog post describing the problem in detail
|
|
496
|
+
- [AssistAgents by OzeroHAX](https://github.com/OzeroHAX/AssistAgents) — hash-based editing for OpenCode with file revision, safe reapply, and structured conflicts
|
|
448
497
|
- [Описание подхода на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — overview of the approach in Russian
|
|
449
498
|
|
|
450
499
|
---
|
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) {
|
|
@@ -104,6 +105,49 @@ if (!result.valid) {
|
|
|
104
105
|
|
|
105
106
|
Верификация хешей использует длину предоставленной хеш-ссылки (а не текущий размер файла), поэтому ссылка вроде `2:f1` остаётся валидной даже если файл вырос.
|
|
106
107
|
|
|
108
|
+
### 🔒 Ревизия файла (`fileRev`)
|
|
109
|
+
|
|
110
|
+
Помимо построчных хешей, hashline вычисляет хеш всего файла (FNV-1a, 8 hex-символов). Он добавляется первой строкой аннотации:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
#HL REV:72c4946c
|
|
114
|
+
#HL 1:a3f|function hello() {
|
|
115
|
+
#HL 2:f1c| return "world";
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
При редактировании передайте `fileRev` в `hashline_edit` — если файл изменился с момента чтения, правка будет отклонена с ошибкой `FILE_REV_MISMATCH`.
|
|
119
|
+
|
|
120
|
+
### 🔄 Safe Reapply
|
|
121
|
+
|
|
122
|
+
Если строка переместилась (например, из-за вставки строк выше), `safeReapply` находит её по хешу контента:
|
|
123
|
+
|
|
124
|
+
- **1 кандидат** — правка применяется к новой позиции
|
|
125
|
+
- **>1 кандидатов** — ошибка `AMBIGUOUS_REAPPLY` (неоднозначность)
|
|
126
|
+
- **0 кандидатов** — ошибка `HASH_MISMATCH`
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const result = applyHashEdit(
|
|
130
|
+
{ operation: "replace", startRef: "1:a3f", replacement: "new" },
|
|
131
|
+
content,
|
|
132
|
+
undefined,
|
|
133
|
+
true, // safeReapply
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 🏷️ Structured Errors
|
|
138
|
+
|
|
139
|
+
Все ошибки hashline — экземпляры `HashlineError` с кодом, диагностикой и подсказками:
|
|
140
|
+
|
|
141
|
+
| Код | Описание |
|
|
142
|
+
|-----|----------|
|
|
143
|
+
| `HASH_MISMATCH` | Содержимое строки изменилось |
|
|
144
|
+
| `FILE_REV_MISMATCH` | Файл модифицирован с момента чтения |
|
|
145
|
+
| `AMBIGUOUS_REAPPLY` | Несколько кандидатов при safe reapply |
|
|
146
|
+
| `TARGET_OUT_OF_RANGE` | Номер строки за пределами файла |
|
|
147
|
+
| `INVALID_REF` | Некорректная хеш-ссылка |
|
|
148
|
+
| `INVALID_RANGE` | Начало диапазона после конца |
|
|
149
|
+
| `MISSING_REPLACEMENT` | Операция replace/insert без содержимого |
|
|
150
|
+
|
|
107
151
|
### 🔍 Чувствительность к отступам
|
|
108
152
|
|
|
109
153
|
Вычисление хеша использует `trimEnd()` (а не `trim()`), поэтому изменения ведущих пробелов (отступов) обнаруживаются как изменения содержимого, а завершающие пробелы игнорируются.
|
|
@@ -113,7 +157,7 @@ if (!result.valid) {
|
|
|
113
157
|
Резолвинг и замена диапазонов строк по хеш-ссылкам:
|
|
114
158
|
|
|
115
159
|
```typescript
|
|
116
|
-
import { resolveRange, replaceRange } from "opencode-hashline";
|
|
160
|
+
import { resolveRange, replaceRange } from "opencode-hashline/utils";
|
|
117
161
|
|
|
118
162
|
// Получить строки между двумя хеш-ссылками
|
|
119
163
|
const range = resolveRange("1:a3f", "3:0e7", content);
|
|
@@ -131,7 +175,7 @@ const newContent = replaceRange(
|
|
|
131
175
|
Создание кастомных экземпляров Hashline с определёнными настройками:
|
|
132
176
|
|
|
133
177
|
```typescript
|
|
134
|
-
import { createHashline } from "opencode-hashline";
|
|
178
|
+
import { createHashline } from "opencode-hashline/utils";
|
|
135
179
|
|
|
136
180
|
const hl = createHashline({
|
|
137
181
|
exclude: ["**/node_modules/**", "**/*.min.js"],
|
|
@@ -151,10 +195,12 @@ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
|
|
|
151
195
|
| Параметр | Тип | По умолчанию | Описание |
|
|
152
196
|
|----------|-----|:------------:|----------|
|
|
153
197
|
| `exclude` | `string[]` | См. ниже | Glob-паттерны для исключения файлов |
|
|
154
|
-
| `maxFileSize` | `number` | `
|
|
198
|
+
| `maxFileSize` | `number` | `1_048_576` (1 МБ) | Макс. размер файла в байтах |
|
|
155
199
|
| `hashLength` | `number \| undefined` | `undefined` (адаптивно) | Принудительная длина хеша |
|
|
156
200
|
| `cacheSize` | `number` | `100` | Макс. файлов в LRU-кеше |
|
|
157
201
|
| `prefix` | `string \| false` | `"#HL "` | Префикс строки (`false` для отключения) |
|
|
202
|
+
| `fileRev` | `boolean` | `true` | Включать ревизию файла (`#HL REV:...`) в аннотации |
|
|
203
|
+
| `safeReapply` | `boolean` | `false` | Автоматический поиск перемещённых строк по хешу |
|
|
158
204
|
|
|
159
205
|
Паттерны исключения по умолчанию: lock-файлы, `node_modules`, минифицированные файлы, бинарные файлы (изображения, шрифты, архивы и т.д.).
|
|
160
206
|
|
|
@@ -417,9 +463,12 @@ npm run typecheck
|
|
|
417
463
|
|
|
418
464
|
Hashline решает эту проблему, присваивая каждой строке короткий детерминированный хеш-тег (например, `2:f1c`), что делает адресацию строк **точной и однозначной**. Модель может ссылаться на любую строку или диапазон без ошибок смещения и путаницы с дубликатами.
|
|
419
465
|
|
|
466
|
+
Продвинутые фичи — **ревизия файла** (`fileRev`), **safe reapply** и **structured errors** — вдохновлены реализацией hash-based editing в проекте **AssistAgents** от [OzeroHAX](https://github.com/OzeroHAX/AssistAgents), который независимо применил аналогичный подход для OpenCode с дополнительными механизмами проверки целостности и диагностики ошибок.
|
|
467
|
+
|
|
420
468
|
**Ссылки:**
|
|
421
469
|
- [oh-my-pi от can1357](https://github.com/can1357/oh-my-pi) — AI-тулкит для разработки: coding agent CLI, unified LLM API, TUI-библиотеки
|
|
422
470
|
- [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) — блог-пост с подробным описанием проблемы
|
|
471
|
+
- [AssistAgents от OzeroHAX](https://github.com/OzeroHAX/AssistAgents) — hash-based editing для OpenCode с file revision, safe reapply и structured conflicts
|
|
423
472
|
- [Статья на Хабре](https://habr.com/ru/companies/bothub/news/995986/) — описание подхода на русском языке
|
|
424
473
|
|
|
425
474
|
---
|
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
|
|