opencode-hashline 1.0.0

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.ru.md ADDED
@@ -0,0 +1,417 @@
1
+ <div align="center">
2
+
3
+ # πŸ”— opencode-hashline
4
+
5
+ **ΠšΠΎΠ½Ρ‚Π΅Π½Ρ‚Π½ΠΎ-адрСсуСмоС Ρ…Π΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ строк для Ρ‚ΠΎΡ‡Π½ΠΎΠ³ΠΎ рСдактирования ΠΊΠΎΠ΄Π° с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ AI**
6
+
7
+ [![CI](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml/badge.svg)](https://github.com/izzzzzi/opencode-hashline/actions/workflows/ci.yml)
8
+ [![npm version](https://img.shields.io/npm/v/opencode-hashline.svg?style=flat&colorA=18181B&colorB=28CF8D)](https://www.npmjs.com/package/opencode-hashline)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat&colorA=18181B&colorB=28CF8D)](LICENSE)
10
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue?style=flat&colorA=18181B&colorB=3178C6)](https://www.typescriptlang.org/)
11
+ [![Node.js](https://img.shields.io/badge/Node.js-ESM-green?style=flat&colorA=18181B&colorB=339933)](https://nodejs.org/)
12
+
13
+ **πŸ‡·πŸ‡Ί Русский** | [πŸ‡¬πŸ‡§ English](README.md)
14
+
15
+ <br />
16
+
17
+ *Hashline-ΠΏΠ»Π°Π³ΠΈΠ½ для [OpenCode](https://github.com/anomalyco/opencode) β€” Π°Π½Π½ΠΎΡ‚ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΊΠ°ΠΆΠ΄ΡƒΡŽ строку Ρ„Π°ΠΉΠ»Π° Π΄Π΅Ρ‚Π΅Ρ€ΠΌΠΈΠ½ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΌ Ρ…Π΅Ρˆ-Ρ‚Π΅Π³ΠΎΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ AI ΠΌΠΎΠ³ ΡΡΡ‹Π»Π°Ρ‚ΡŒΡΡ Π½Π° ΠΊΠΎΠ΄ ΠΈ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π΅Π³ΠΎ с хирургичСской Ρ‚ΠΎΡ‡Π½ΠΎΡΡ‚ΡŒΡŽ.*
18
+
19
+ </div>
20
+
21
+ ---
22
+
23
+ ## πŸ“– Π§Ρ‚ΠΎ Ρ‚Π°ΠΊΠΎΠ΅ Hashline?
24
+
25
+ Hashline Π°Π½Π½ΠΎΡ‚ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΊΠ°ΠΆΠ΄ΡƒΡŽ строку Ρ„Π°ΠΉΠ»Π° ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΈΠΌ Π΄Π΅Ρ‚Π΅Ρ€ΠΌΠΈΠ½ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΌ hex-Ρ…Π΅ΡˆΠ΅ΠΌ. Когда AI Ρ‡ΠΈΡ‚Π°Π΅Ρ‚ Ρ„Π°ΠΉΠ», ΠΎΠ½ Π²ΠΈΠ΄ΠΈΡ‚:
26
+
27
+ ```
28
+ #HL 1:a3f|function hello() {
29
+ #HL 2:f1c| return "world";
30
+ #HL 3:0e7|}
31
+ ```
32
+
33
+ > **ΠŸΡ€ΠΈΠΌΠ΅Ρ‡Π°Π½ΠΈΠ΅:** Π”Π»ΠΈΠ½Π° Ρ…Π΅ΡˆΠ° адаптивная β€” ΠΎΠ½Π° зависит ΠΎΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Ρ„Π°ΠΉΠ»Π° (2 символа для ≀256 строк, 3 символа для ≀4096 строк, 4 символа для >4096 строк). Π’ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π°Ρ… Π½ΠΈΠΆΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ΡΡ 3-ΡΠΈΠΌΠ²ΠΎΠ»ΡŒΠ½Ρ‹Π΅ Ρ…Π΅ΡˆΠΈ. ΠŸΡ€Π΅Ρ„ΠΈΠΊΡ `#HL ` Π·Π°Ρ‰ΠΈΡ‰Π°Π΅Ρ‚ ΠΎΡ‚ Π»ΠΎΠΆΠ½Ρ‹Ρ… срабатываний ΠΏΡ€ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΈ Ρ…Π΅ΡˆΠ΅ΠΉ ΠΈ являСтся настраиваСмым.
34
+
35
+ AI-модСль ΠΌΠΎΠΆΠ΅Ρ‚ ΡΡΡ‹Π»Π°Ρ‚ΡŒΡΡ Π½Π° строки ΠΏΠΎ ΠΈΡ… Ρ…Π΅Ρˆ-Ρ‚Π΅Π³Π°ΠΌ для Ρ‚ΠΎΡ‡Π½ΠΎΠ³ΠΎ рСдактирования:
36
+
37
+ - **Β«Π—Π°ΠΌΠ΅Π½ΠΈΡ‚ΡŒ строку `2:f1c`Β»** β€” ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½ΡƒΡŽ строку ΠΎΠ΄Π½ΠΎΠ·Π½Π°Ρ‡Π½ΠΎ
38
+ - **Β«Π—Π°ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Π±Π»ΠΎΠΊ ΠΎΡ‚ `1:a3f` Π΄ΠΎ `3:0e7`Β»** β€” ΡƒΠΊΠ°Π·Π°Ρ‚ΡŒ Π΄ΠΈΠ°ΠΏΠ°Π·ΠΎΠ½ строк
39
+ - **Β«Π’ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ послС `3:0e7`Β»** β€” Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ Π² Ρ‚ΠΎΡ‡Π½ΠΎΠ΅ мСсто
40
+
41
+ ### πŸ€” ΠŸΠΎΡ‡Π΅ΠΌΡƒ это ΠΏΠΎΠΌΠΎΠ³Π°Π΅Ρ‚?
42
+
43
+ Π’Ρ€Π°Π΄ΠΈΡ†ΠΈΠΎΠ½Π½Ρ‹Π΅ Π½ΠΎΠΌΠ΅Ρ€Π° строк ΡΠ΄Π²ΠΈΠ³Π°ΡŽΡ‚ΡΡ ΠΏΡ€ΠΈ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ, вызывая ошибки смСщСния ΠΈ ΡƒΡΡ‚Π°Ρ€Π΅Π²ΡˆΠΈΠ΅ ссылки. Π₯Сш-Ρ‚Π΅Π³ΠΈ Hashline **ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚Π½ΠΎ-адрСсуСмы** β€” ΠΎΠ½ΠΈ Π²Ρ‹Ρ‡ΠΈΡΠ»ΡΡŽΡ‚ΡΡ ΠΈΠ· индСкса строки ΠΈ Π΅Ρ‘ содСрТимого, Ρ‡Ρ‚ΠΎ Π΄Π΅Π»Π°Π΅Ρ‚ ΠΈΡ… ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½ΠΎΠΉ, Π²Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΡƒΠ΅ΠΌΠΎΠΉ ссылкой для Ρ‚ΠΎΡ‡Π½ΠΎΠΉ ΠΊΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΠΈ ΠΎ мСстополоТСнии Π² ΠΊΠΎΠ΄Π΅.
44
+
45
+ ---
46
+
47
+ ## ✨ ВозмоТности
48
+
49
+ ### πŸ“ Адаптивная Π΄Π»ΠΈΠ½Π° Ρ…Π΅ΡˆΠ°
50
+
51
+ Π”Π»ΠΈΠ½Π° Ρ…Π΅ΡˆΠ° автоматичСски адаптируСтся ΠΊ Ρ€Π°Π·ΠΌΠ΅Ρ€Ρƒ Ρ„Π°ΠΉΠ»Π° для ΠΌΠΈΠ½ΠΈΠΌΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΉ:
52
+
53
+ | Π Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° | Π”Π»ΠΈΠ½Π° Ρ…Π΅ΡˆΠ° | Π’ΠΎΠ·ΠΌΠΎΠΆΠ½Ρ‹Ρ… Π·Π½Π°Ρ‡Π΅Π½ΠΈΠΉ |
54
+ |-------------|:----------:|:------------------:|
55
+ | ≀ 256 строк | 2 hex-символа | 256 |
56
+ | ≀ 4 096 строк | 3 hex-символа | 4 096 |
57
+ | > 4 096 строк | 4 hex-символа | 65 536 |
58
+
59
+ ### 🏷️ ΠœΠ°Π³ΠΈΡ‡Π΅ΡΠΊΠΈΠΉ прСфикс (`#HL `)
60
+
61
+ Π‘Ρ‚Ρ€ΠΎΠΊΠΈ Π°Π½Π½ΠΎΡ‚ΠΈΡ€ΡƒΡŽΡ‚ΡΡ настраиваСмым прСфиксом (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ: `#HL `), Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‚ΠΈΡ‚ΡŒ Π»ΠΎΠΆΠ½Ρ‹Π΅ срабатывания ΠΏΡ€ΠΈ ΡƒΠ΄Π°Π»Π΅Π½ΠΈΠΈ Ρ…Π΅ΡˆΠ΅ΠΉ. Π­Ρ‚ΠΎ Π³Π°Ρ€Π°Π½Ρ‚ΠΈΡ€ΡƒΠ΅Ρ‚, Ρ‡Ρ‚ΠΎ строки Π΄Π°Π½Π½Ρ‹Ρ… Π²Ρ€ΠΎΠ΄Π΅ `1:ab|some data` Π½Π΅ Π±ΡƒΠ΄ΡƒΡ‚ случайно ΠΎΠ±Ρ€Π΅Π·Π°Π½Ρ‹.
62
+
63
+ ```
64
+ #HL 1:a3|function hello() {
65
+ #HL 2:f1| return "world";
66
+ #HL 3:0e|}
67
+ ```
68
+
69
+ ΠŸΡ€Π΅Ρ„ΠΈΠΊΡ ΠΌΠΎΠΆΠ½ΠΎ Π½Π°ΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ ΠΈΠ»ΠΈ ΠΎΡ‚ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ для ΠΎΠ±Ρ€Π°Ρ‚Π½ΠΎΠΉ совмСстимости:
70
+
71
+ ```typescript
72
+ // ΠšΠ°ΡΡ‚ΠΎΠΌΠ½Ρ‹ΠΉ прСфикс
73
+ const hl = createHashline({ prefix: ">> " });
74
+
75
+ // ΠžΡ‚ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ прСфикс (legacy-Ρ„ΠΎΡ€ΠΌΠ°Ρ‚: "1:a3|code")
76
+ const hl = createHashline({ prefix: false });
77
+ ```
78
+
79
+ ### πŸ’Ύ LRU-ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅
80
+
81
+ ВстроСнный LRU-кСш (`filePath β†’ annotatedContent`) с настраиваСмым Ρ€Π°Π·ΠΌΠ΅Ρ€ΠΎΠΌ (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ 100 Ρ„Π°ΠΉΠ»ΠΎΠ²). ΠŸΡ€ΠΈ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΌ Ρ‡Ρ‚Π΅Π½ΠΈΠΈ Ρ‚ΠΎΠ³ΠΎ ΠΆΠ΅ Ρ„Π°ΠΉΠ»Π° с Π½Π΅ΠΈΠ·ΠΌΠ΅Π½Ρ‘Π½Π½Ρ‹ΠΌ содСрТимым возвращаСтся ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚. КСш автоматичСски инвалидируСтся ΠΏΡ€ΠΈ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΈ содСрТимого Ρ„Π°ΠΉΠ»Π°.
82
+
83
+ ### βœ… ВСрификация Ρ…Π΅ΡˆΠ΅ΠΉ
84
+
85
+ ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΎΠ³ΠΎ, Ρ‡Ρ‚ΠΎ строка Π½Π΅ измСнилась с ΠΌΠΎΠΌΠ΅Π½Ρ‚Π° чтСния β€” Π·Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ race conditions:
86
+
87
+ ```typescript
88
+ import { verifyHash } from "opencode-hashline";
89
+
90
+ const result = verifyHash(2, "f1c", currentContent);
91
+ if (!result.valid) {
92
+ console.error(result.message); // "Hash mismatch at line 2: ..."
93
+ }
94
+ ```
95
+
96
+ ВСрификация Ρ…Π΅ΡˆΠ΅ΠΉ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Π΄Π»ΠΈΠ½Ρƒ прСдоставлСнной Ρ…Π΅Ρˆ-ссылки (Π° Π½Π΅ Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π°), поэтому ссылка Π²Ρ€ΠΎΠ΄Π΅ `2:f1` остаётся Π²Π°Π»ΠΈΠ΄Π½ΠΎΠΉ Π΄Π°ΠΆΠ΅ Ссли Ρ„Π°ΠΉΠ» вырос.
97
+
98
+ ### πŸ” Π§ΡƒΠ²ΡΡ‚Π²ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ ΠΊ отступам
99
+
100
+ ВычислСниС Ρ…Π΅ΡˆΠ° ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ `trimEnd()` (Π° Π½Π΅ `trim()`), поэтому измСнСния Π²Π΅Π΄ΡƒΡ‰ΠΈΡ… ΠΏΡ€ΠΎΠ±Π΅Π»ΠΎΠ² (отступов) ΠΎΠ±Π½Π°Ρ€ΡƒΠΆΠΈΠ²Π°ΡŽΡ‚ΡΡ ΠΊΠ°ΠΊ измСнСния содСрТимого, Π° Π·Π°Π²Π΅Ρ€ΡˆΠ°ΡŽΡ‰ΠΈΠ΅ ΠΏΡ€ΠΎΠ±Π΅Π»Ρ‹ ΠΈΠ³Π½ΠΎΡ€ΠΈΡ€ΡƒΡŽΡ‚ΡΡ.
101
+
102
+ ### πŸ“ Range-ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ
103
+
104
+ Π Π΅Π·ΠΎΠ»Π²ΠΈΠ½Π³ ΠΈ Π·Π°ΠΌΠ΅Π½Π° Π΄ΠΈΠ°ΠΏΠ°Π·ΠΎΠ½ΠΎΠ² строк ΠΏΠΎ Ρ…Π΅Ρˆ-ссылкам:
105
+
106
+ ```typescript
107
+ import { resolveRange, replaceRange } from "opencode-hashline";
108
+
109
+ // ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ строки ΠΌΠ΅ΠΆΠ΄Ρƒ двумя Ρ…Π΅Ρˆ-ссылками
110
+ const range = resolveRange("1:a3f", "3:0e7", content);
111
+ console.log(range.lines); // ["function hello() {", ' return "world";', "}"]
112
+
113
+ // Π—Π°ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Π΄ΠΈΠ°ΠΏΠ°Π·ΠΎΠ½ Π½ΠΎΠ²Ρ‹ΠΌ содСрТимым
114
+ const newContent = replaceRange(
115
+ "1:a3f", "3:0e7", content,
116
+ "function goodbye() {\n return 'farewell';\n}"
117
+ );
118
+ ```
119
+
120
+ ### βš™οΈ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€ΠΈΡ€ΡƒΠ΅ΠΌΠΎΡΡ‚ΡŒ
121
+
122
+ Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ кастомных экзСмпляров Hashline с ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½Ρ‹ΠΌΠΈ настройками:
123
+
124
+ ```typescript
125
+ import { createHashline } from "opencode-hashline";
126
+
127
+ const hl = createHashline({
128
+ exclude: ["**/node_modules/**", "**/*.min.js"],
129
+ maxFileSize: 512_000, // 512 ΠšΠ‘
130
+ hashLength: 3, // ΠΏΡ€ΠΈΠ½ΡƒΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ 3-ΡΠΈΠΌΠ²ΠΎΠ»ΡŒΠ½Ρ‹Π΅ Ρ…Π΅ΡˆΠΈ
131
+ cacheSize: 200, // ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π΄ΠΎ 200 Ρ„Π°ΠΉΠ»ΠΎΠ²
132
+ prefix: "#HL ", // магичСский прСфикс (ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ)
133
+ });
134
+
135
+ // ИспользованиС настроСнного экзСмпляра
136
+ const annotated = hl.formatFileWithHashes(content, "src/app.ts");
137
+ const isExcluded = hl.shouldExclude("node_modules/foo.js"); // true
138
+ ```
139
+
140
+ #### ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ
141
+
142
+ | ΠŸΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ | Π’ΠΈΠΏ | По ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ | ОписаниС |
143
+ |----------|-----|:------------:|----------|
144
+ | `exclude` | `string[]` | Π‘ΠΌ. Π½ΠΈΠΆΠ΅ | Glob-ΠΏΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ для ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ Ρ„Π°ΠΉΠ»ΠΎΠ² |
145
+ | `maxFileSize` | `number` | `1_000_000` | Макс. Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° Π² Π±Π°ΠΉΡ‚Π°Ρ… |
146
+ | `hashLength` | `number \| undefined` | `undefined` (Π°Π΄Π°ΠΏΡ‚ΠΈΠ²Π½ΠΎ) | ΠŸΡ€ΠΈΠ½ΡƒΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ Π΄Π»ΠΈΠ½Π° Ρ…Π΅ΡˆΠ° |
147
+ | `cacheSize` | `number` | `100` | Макс. Ρ„Π°ΠΉΠ»ΠΎΠ² Π² LRU-кСшС |
148
+ | `prefix` | `string \| false` | `"#HL "` | ΠŸΡ€Π΅Ρ„ΠΈΠΊΡ строки (`false` для ΠΎΡ‚ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ) |
149
+
150
+ ΠŸΠ°Ρ‚Ρ‚Π΅Ρ€Π½Ρ‹ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΡ ΠΏΠΎ ΡƒΠΌΠΎΠ»Ρ‡Π°Π½ΠΈΡŽ: lock-Ρ„Π°ΠΉΠ»Ρ‹, `node_modules`, ΠΌΠΈΠ½ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ„Π°ΠΉΠ»Ρ‹, Π±ΠΈΠ½Π°Ρ€Π½Ρ‹Π΅ Ρ„Π°ΠΉΠ»Ρ‹ (изобраТСния, ΡˆΡ€ΠΈΡ„Ρ‚Ρ‹, Π°Ρ€Ρ…ΠΈΠ²Ρ‹ ΠΈ Ρ‚.Π΄.).
151
+
152
+ ---
153
+
154
+ ## πŸ“¦ Установка
155
+
156
+ ```bash
157
+ npm install opencode-hashline
158
+ ```
159
+
160
+ ---
161
+
162
+ ## πŸ”§ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ
163
+
164
+ Π”ΠΎΠ±Π°Π²ΡŒΡ‚Π΅ ΠΏΠ»Π°Π³ΠΈΠ½ Π² ваш `opencode.json`:
165
+
166
+ ```json
167
+ {
168
+ "$schema": "https://opencode.ai/config.json",
169
+ "plugin": ["opencode-hashline"]
170
+ }
171
+ ```
172
+
173
+ ### Π€Π°ΠΉΠ»Ρ‹ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ
174
+
175
+ Плагин Π·Π°Π³Ρ€ΡƒΠΆΠ°Π΅Ρ‚ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ ΠΈΠ· ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΡ… мСст (Π² порядкС ΠΏΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚Π°, Π±ΠΎΠ»Π΅Π΅ ΠΏΠΎΠ·Π΄Π½ΠΈΠ΅ ΠΏΠ΅Ρ€Π΅Π·Π°ΠΏΠΈΡΡ‹Π²Π°ΡŽΡ‚ Ρ€Π°Π½Π½ΠΈΠ΅):
176
+
177
+ | ΠŸΡ€ΠΈΠΎΡ€ΠΈΡ‚Π΅Ρ‚ | РасполоТСниС | ΠžΠ±Π»Π°ΡΡ‚ΡŒ |
178
+ |:---------:|-------------|---------|
179
+ | 1 | `~/.config/opencode/opencode-hashline.json` | Π“Π»ΠΎΠ±Π°Π»ΡŒΠ½Π°Ρ (всС ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Ρ‹) |
180
+ | 2 | `<project>/opencode-hashline.json` | Π›ΠΎΠΊΠ°Π»ΡŒΠ½Π°Ρ (ΠΏΡ€ΠΎΠ΅ΠΊΡ‚) |
181
+ | 3 | ΠŸΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ½Π°Ρ конфигурация Ρ‡Π΅Ρ€Π΅Π· `createHashlinePlugin()` | АргумСнт Ρ„Π°Π±Ρ€ΠΈΠΊΠΈ |
182
+
183
+ ΠŸΡ€ΠΈΠΌΠ΅Ρ€ `opencode-hashline.json`:
184
+
185
+ ```json
186
+ {
187
+ "exclude": ["**/node_modules/**", "**/*.min.js"],
188
+ "maxFileSize": 1048576,
189
+ "hashLength": 0,
190
+ "cacheSize": 100,
191
+ "prefix": "#HL "
192
+ }
193
+ ```
194
+
195
+ Π’ΠΎΡ‚ ΠΈ всё! Плагин автоматичСски:
196
+
197
+ | # | ДСйствиС | ОписаниС |
198
+ |:-:|----------|----------|
199
+ | 1 | πŸ“ **АннотируСт Ρ‡Ρ‚Π΅Π½ΠΈΠ΅ Ρ„Π°ΠΉΠ»ΠΎΠ²** | ΠŸΡ€ΠΈ Ρ‡Ρ‚Π΅Π½ΠΈΠΈ Ρ„Π°ΠΉΠ»Π° AI каТдая строка ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ `#HL` Ρ…Π΅Ρˆ-прСфикс |
200
+ | 2 | πŸ“Ž **АннотируСт `@file` упоминания** | Π€Π°ΠΉΠ»Ρ‹, ΠΏΡ€ΠΈΠΊΡ€Π΅ΠΏΠ»Ρ‘Π½Π½Ρ‹Π΅ Ρ‡Π΅Ρ€Π΅Π· `@filename` Π² ΠΏΡ€ΠΎΠΌΠΏΡ‚Π΅, Ρ‚ΠΎΠΆΠ΅ Π°Π½Π½ΠΎΡ‚ΠΈΡ€ΡƒΡŽΡ‚ΡΡ Ρ…Π΅ΡˆΠ»Π°ΠΉΠ½Π°ΠΌΠΈ |
201
+ | 3 | βœ‚οΈ **Π£Π±ΠΈΡ€Π°Π΅Ρ‚ Ρ…Π΅Ρˆ-прСфиксы ΠΏΡ€ΠΈ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ** | ΠŸΡ€ΠΈ записи/Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ Ρ„Π°ΠΉΠ»Π° Ρ…Π΅Ρˆ-прСфиксы ΡƒΠ΄Π°Π»ΡΡŽΡ‚ΡΡ ΠΏΠ΅Ρ€Π΅Π΄ ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ΠΌ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ |
202
+ | 4 | 🧠 **ВнСдряСт инструкции Π² систСмный ΠΏΡ€ΠΎΠΌΠΏΡ‚** | AI ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅Ρ‚ инструкции ΠΏΠΎ ΠΈΠ½Ρ‚Π΅Ρ€ΠΏΡ€Π΅Ρ‚Π°Ρ†ΠΈΠΈ ΠΈ использованию hashline-ссылок |
203
+ | 5 | πŸ’Ύ **ΠšΠ΅ΡˆΠΈΡ€ΡƒΠ΅Ρ‚ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Ρ‹** | ΠŸΠΎΠ²Ρ‚ΠΎΡ€Π½Ρ‹Π΅ чтСния Ρ‚ΠΎΠ³ΠΎ ΠΆΠ΅ Ρ„Π°ΠΉΠ»Π° Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‚ ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Π°Π½Π½ΠΎΡ‚Π°Ρ†ΠΈΠΈ |
204
+ | 6 | πŸ” **Π€ΠΈΠ»ΡŒΡ‚Ρ€ΡƒΠ΅Ρ‚ ΠΏΠΎ инструмСнту** | Волько инструмСнты чтСния Ρ„Π°ΠΉΠ»ΠΎΠ² (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ `read_file`, `cat`, `view`) ΠΏΠΎΠ»ΡƒΡ‡Π°ΡŽΡ‚ Π°Π½Π½ΠΎΡ‚Π°Ρ†ΠΈΠΈ; ΠΎΡΡ‚Π°Π»ΡŒΠ½Ρ‹Π΅ Π½Π΅ Π·Π°Ρ‚Ρ€Π°Π³ΠΈΠ²Π°ΡŽΡ‚ΡΡ |
205
+ | 7 | βš™οΈ **Π£Ρ‡ΠΈΡ‚Ρ‹Π²Π°Π΅Ρ‚ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ** | Π˜ΡΠΊΠ»ΡŽΡ‡Ρ‘Π½Π½Ρ‹Π΅ Ρ„Π°ΠΉΠ»Ρ‹ ΠΈ Ρ„Π°ΠΉΠ»Ρ‹, ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ°ΡŽΡ‰ΠΈΠ΅ `maxFileSize`, ΠΏΡ€ΠΎΠΏΡƒΡΠΊΠ°ΡŽΡ‚ΡΡ |
206
+ | 8 | 🧩 **РСгистрируСт `hashline_edit` tool** | ΠŸΡ€ΠΈΠΌΠ΅Π½ΡΠ΅Ρ‚ replace/delete/insert ΠΏΠΎ hash-ссылкам Π±Π΅Π· Ρ‚ΠΎΡ‡Π½ΠΎΠ³ΠΎ `old_string`-ΠΌΠ°Ρ‚Ρ‡ΠΈΠ½Π³Π° |
207
+
208
+ ---
209
+
210
+ ## πŸ› οΈ Как это Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚
211
+
212
+ ### ВычислСниС Ρ…Π΅ΡˆΠ°
213
+
214
+ Π₯Сш ΠΊΠ°ΠΆΠ΄ΠΎΠΉ строки вычисляСтся ΠΈΠ·:
215
+ - **0-based индСкса** строки
216
+ - **Π‘ΠΎΠ΄Π΅Ρ€ΠΆΠΈΠΌΠΎΠ³ΠΎ строки** с ΠΎΠ±Ρ€Π΅Π·Π°Π½Π½Ρ‹ΠΌΠΈ Π·Π°Π²Π΅Ρ€ΡˆΠ°ΡŽΡ‰ΠΈΠΌΠΈ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ (trimEnd) β€” Π²Π΅Π΄ΡƒΡ‰ΠΈΠ΅ ΠΏΡ€ΠΎΠ±Π΅Π»Ρ‹ (отступы) Π—ΠΠΠ§Π˜ΠœΠ«
217
+
218
+ Π­Ρ‚ΠΎ подаётся Π² Ρ…Π΅Ρˆ-Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ **FNV-1a**, сводится ΠΊ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰Π΅ΠΌΡƒ ΠΌΠΎΠ΄ΡƒΠ»ΡŽ Π² зависимости ΠΎΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Ρ„Π°ΠΉΠ»Π° ΠΈ отобраТаСтся ΠΊΠ°ΠΊ hex-строка.
219
+
220
+ ### Π₯ΡƒΠΊΠΈ ΠΈ tool ΠΏΠ»Π°Π³ΠΈΠ½Π°
221
+
222
+ Плагин рСгистрируСт Ρ‡Π΅Ρ‚Ρ‹Ρ€Π΅ Ρ…ΡƒΠΊΠ° OpenCode ΠΈ ΠΎΠ΄ΠΈΠ½ кастомный tool:
223
+
224
+ | Π₯ΡƒΠΊ | НазначСниС |
225
+ |-----|-----------|
226
+ | `tool.hashline_edit` | Hash-aware ΠΏΡ€Π°Π²ΠΊΠΈ ΠΏΠΎ ссылкам Π²Ρ€ΠΎΠ΄Π΅ `5:a3f` ΠΈΠ»ΠΈ `#HL 5:a3f|...` |
227
+ | `tool.execute.after` | ДобавляСт hashline-Π°Π½Π½ΠΎΡ‚Π°Ρ†ΠΈΠΈ Π² Π²Ρ‹Π²ΠΎΠ΄ инструмСнтов чтСния Ρ„Π°ΠΉΠ»ΠΎΠ² |
228
+ | `tool.execute.before` | Π£Π±ΠΈΡ€Π°Π΅Ρ‚ hashline-прСфиксы ΠΈΠ· Π°Ρ€Π³ΡƒΠΌΠ΅Π½Ρ‚ΠΎΠ² инструмСнтов рСдактирования |
229
+ | `chat.message` | АннотируСт `@file` упоминания Π² сообщСниях ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ (записываСт Π°Π½Π½ΠΎΡ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ ΠΊΠΎΠ½Ρ‚Π΅Π½Ρ‚ Π²ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» ΠΈ подмСняСт URL) |
230
+ | `experimental.chat.system.transform` | ДобавляСт инструкции ΠΏΠΎ использованию hashline Π² систСмный ΠΏΡ€ΠΎΠΌΠΏΡ‚ |
231
+
232
+ ---
233
+
234
+ ## πŸ”Œ ΠŸΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ½Ρ‹ΠΉ API
235
+
236
+ ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ ΡƒΡ‚ΠΈΠ»ΠΈΡ‚Ρ‹ ΡΠΊΡΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΡŽΡ‚ΡΡ ΠΈΠ· субпути `opencode-hashline/utils` (Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ ΠΊΠΎΠ½Ρ„Π»ΠΈΠΊΡ‚ΠΎΠ² с Π·Π°Π³Ρ€ΡƒΠ·Ρ‡ΠΈΠΊΠΎΠΌ ΠΏΠ»Π°Π³ΠΈΠ½ΠΎΠ² OpenCode, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²Ρ‹Π·Ρ‹Π²Π°Π΅Ρ‚ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ экспорт ΠΊΠ°ΠΊ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ Plugin):
237
+
238
+ ```typescript
239
+ import {
240
+ computeLineHash,
241
+ formatFileWithHashes,
242
+ stripHashes,
243
+ parseHashRef,
244
+ normalizeHashRef,
245
+ buildHashMap,
246
+ getAdaptiveHashLength,
247
+ verifyHash,
248
+ resolveRange,
249
+ replaceRange,
250
+ applyHashEdit,
251
+ HashlineCache,
252
+ createHashline,
253
+ shouldExclude,
254
+ matchesGlob,
255
+ resolveConfig,
256
+ DEFAULT_PREFIX,
257
+ } from "opencode-hashline/utils";
258
+ ```
259
+
260
+ ### ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ
261
+
262
+ ```typescript
263
+ // Π’Ρ‹Ρ‡ΠΈΡΠ»ΠΈΡ‚ΡŒ Ρ…Π΅Ρˆ для ΠΎΠ΄Π½ΠΎΠΉ строки
264
+ const hash = computeLineHash(0, "function hello() {"); // Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ "a3f"
265
+
266
+ // Π’Ρ‹Ρ‡ΠΈΡΠ»ΠΈΡ‚ΡŒ Ρ…Π΅Ρˆ с ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠΉ Π΄Π»ΠΈΠ½ΠΎΠΉ
267
+ const hash4 = computeLineHash(0, "function hello() {", 4); // Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ "a3f2"
268
+
269
+ // ΠΠ½Π½ΠΎΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ содСрТимоС Ρ„Π°ΠΉΠ»Π° (адаптивная Π΄Π»ΠΈΠ½Π° Ρ…Π΅ΡˆΠ°, с прСфиксом #HL)
270
+ const annotated = formatFileWithHashes(fileContent);
271
+ // "#HL 1:a3|function hello() {\n#HL 2:f1| return \"world\";\n#HL 3:0e|}"
272
+
273
+ // ΠΠ½Π½ΠΎΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ с ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠΉ Π΄Π»ΠΈΠ½ΠΎΠΉ Ρ…Π΅ΡˆΠ°
274
+ const annotated3 = formatFileWithHashes(fileContent, 3);
275
+
276
+ // ΠΠ½Π½ΠΎΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Π±Π΅Π· прСфикса (legacy-Ρ„ΠΎΡ€ΠΌΠ°Ρ‚)
277
+ const annotatedLegacy = formatFileWithHashes(fileContent, undefined, false);
278
+
279
+ // Π£Π±Ρ€Π°Ρ‚ΡŒ Π°Π½Π½ΠΎΡ‚Π°Ρ†ΠΈΠΈ, ΠΏΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π»ΡŒΠ½ΠΎΠ΅ содСрТимоС
280
+ const original = stripHashes(annotated);
281
+ ```
282
+
283
+ ### Π₯Сш-ссылки ΠΈ вСрификация
284
+
285
+ ```typescript
286
+ // Π Π°Π·ΠΎΠ±Ρ€Π°Ρ‚ΡŒ Ρ…Π΅Ρˆ-ссылку
287
+ const { line, hash } = parseHashRef("2:f1c"); // { line: 2, hash: "f1c" }
288
+
289
+ // ΠΠΎΡ€ΠΌΠ°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ссылку ΠΈΠ· Π°Π½Π½ΠΎΡ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½ΠΎΠΉ строки
290
+ const ref = normalizeHashRef("#HL 2:f1c|const x = 1;"); // "2:f1c"
291
+
292
+ // ΠŸΠΎΡΡ‚Ρ€ΠΎΠΈΡ‚ΡŒ ΠΊΠ°Ρ€Ρ‚Ρƒ соотвСтствий
293
+ const map = buildHashMap(fileContent); // Map<"2:f1c", 2>
294
+
295
+ // Π’Π΅Ρ€ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ…Π΅Ρˆ-ссылку (ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ hash.length, Π° Π½Π΅ Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π°)
296
+ const result = verifyHash(2, "f1c", fileContent);
297
+ ```
298
+
299
+ ### Range-ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΈ
300
+
301
+ ```typescript
302
+ // Π Π΅Π·ΠΎΠ»Π²ΠΈΡ‚ΡŒ Π΄ΠΈΠ°ΠΏΠ°Π·ΠΎΠ½
303
+ const range = resolveRange("1:a3f", "3:0e7", fileContent);
304
+
305
+ // Π—Π°ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Π΄ΠΈΠ°ΠΏΠ°Π·ΠΎΠ½
306
+ const newContent = replaceRange("1:a3f", "3:0e7", fileContent, "Π½ΠΎΠ²ΠΎΠ΅ содСрТимоС");
307
+
308
+ // Hash-aware опСрация рСдактирования (replace/delete/insert_before/insert_after)
309
+ const edited = applyHashEdit(
310
+ { operation: "replace", startRef: "1:a3f", endRef: "3:0e7", replacement: "Π½ΠΎΠ²ΠΎΠ΅ содСрТимоС" },
311
+ fileContent
312
+ ).content;
313
+ ```
314
+
315
+ ### Π£Ρ‚ΠΈΠ»ΠΈΡ‚Ρ‹
316
+
317
+ ```typescript
318
+ // ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ, Π½ΡƒΠΆΠ½ΠΎ Π»ΠΈ ΠΈΡΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Ρ„Π°ΠΉΠ»
319
+ const excluded = shouldExclude("node_modules/foo.js", ["**/node_modules/**"]);
320
+
321
+ // Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ настроСнный экзСмпляр
322
+ const hl = createHashline({ cacheSize: 50, hashLength: 3 });
323
+ ```
324
+
325
+ ---
326
+
327
+ ## πŸ“Š Π‘Π΅Π½Ρ‡ΠΌΠ°Ρ€ΠΊ
328
+
329
+ ### ΠšΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎΡΡ‚ΡŒ: hashline vs str_replace
330
+
331
+ Оба ΠΏΠΎΠ΄Ρ…ΠΎΠ΄Π° протСстированы Π½Π° **60 фикстурах ΠΈΠ· [react-edit-benchmark](https://github.com/can1357/oh-my-pi/tree/main/packages/react-edit-benchmark)** β€” ΠΌΡƒΡ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Ρ… Ρ„Π°ΠΉΠ»Π°Ρ… React с извСстными Π±Π°Π³Π°ΠΌΠΈ (ΠΈΠ½Π²Π΅Ρ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Π±ΡƒΠ»Π΅Π²Ρ‹, ΠΏΠ΅Ρ€Π΅ΠΏΡƒΡ‚Π°Π½Π½Ρ‹Π΅ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Ρ‹, ΡƒΠ΄Π°Π»Ρ‘Π½Π½Ρ‹Π΅ guard-ΠΊΠ»Π°ΡƒΠ·Ρ‹ ΠΈ Ρ‚.Π΄.):
332
+
333
+ | | hashline | str_replace |
334
+ |---|:---:|:---:|
335
+ | **ΠŸΡ€ΠΎΡˆΠ»ΠΎ** | **60/60 (100%)** | 58/60 (96.7%) |
336
+ | **ΠŸΡ€ΠΎΠ²Π°Π»Π΅Π½ΠΎ** | 0 | 2 |
337
+ | **НСоднозначныС ΠΏΡ€Π°Π²ΠΊΠΈ** | 0 | 4 |
338
+
339
+ str_replace ломаСтся, ΠΊΠΎΠ³Π΄Π° `old_string` встрСчаСтся Π² Ρ„Π°ΠΉΠ»Π΅ нСсколько Ρ€Π°Π· (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ΠΏΠΎΠ²Ρ‚ΠΎΡ€ΡΡŽΡ‰ΠΈΠ΅ΡΡ guard-ΠΊΠ»Π°ΡƒΠ·Ρ‹, ΠΏΠΎΡ…ΠΎΠΆΠΈΠ΅ Π±Π»ΠΎΠΊΠΈ ΠΊΠΎΠ΄Π°). Hashline адрСсуСт ΠΊΠ°ΠΆΠ΄ΡƒΡŽ строку ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½ΠΎ Ρ‡Π΅Ρ€Π΅Π· `lineNumber:hash`, поэтому Π½Π΅ΠΎΠ΄Π½ΠΎΠ·Π½Π°Ρ‡Π½ΠΎΡΡ‚ΡŒ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½Π°.
340
+
341
+ ```bash
342
+ # ЗапуститС сами:
343
+ npx tsx benchmark/run.ts # Ρ€Π΅ΠΆΠΈΠΌ hashline
344
+ npx tsx benchmark/run.ts --no-hash # Ρ€Π΅ΠΆΠΈΠΌ str_replace
345
+ ```
346
+
347
+ <details>
348
+ <summary>Ошибки str_replace (катСгория structural)</summary>
349
+
350
+ - `structural-remove-early-return-001` β€” `old_string` совпал Π² Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… мСстах, Π·Π°ΠΌΠ΅Π½Π° ΠΏΡ€ΠΈΠΌΠ΅Π½Π΅Π½Π° Π½Π΅ ΠΊ Ρ‚ΠΎΠΌΡƒ
351
+ - `structural-remove-early-return-002` β€” аналогичная ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ°
352
+ - `structural-delete-statement-002` β€” Π½Π΅ΠΎΠ΄Π½ΠΎΠ·Π½Π°Ρ‡Π½ΠΎΠ΅ совпадСниС (ΠΏΠ΅Ρ€Π²ΠΎΠ΅ совпадСниС оказалось Π²Π΅Ρ€Π½Ρ‹ΠΌ)
353
+ - `structural-delete-statement-003` β€” Π½Π΅ΠΎΠ΄Π½ΠΎΠ·Π½Π°Ρ‡Π½ΠΎΠ΅ совпадСниС (ΠΏΠ΅Ρ€Π²ΠΎΠ΅ совпадСниС оказалось Π²Π΅Ρ€Π½Ρ‹ΠΌ)
354
+
355
+ </details>
356
+
357
+ ### Расход Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²
358
+
359
+ Аннотации hashline Π΄ΠΎΠ±Π°Π²Π»ΡΡŽΡ‚ прСфикс `#HL <line>:<hash>|` (~12 символов / ~3 Ρ‚ΠΎΠΊΠ΅Π½Π°) Π½Π° строку:
360
+
361
+ | | Π‘Π΅Π· Ρ…Π΅ΡˆΠ΅ΠΉ | Π‘ Ρ…Π΅ΡˆΠ°ΠΌΠΈ | ΠžΠ²Π΅Ρ€Ρ…Π΅Π΄ |
362
+ |---|---:|---:|:---:|
363
+ | **Π‘ΠΈΠΌΠ²ΠΎΠ»Ρ‹** | 404K | 564K | +40% |
364
+ | **Π’ΠΎΠΊΠ΅Π½Ρ‹ (~)** | ~101K | ~141K | +40% |
365
+
366
+ ΠžΠ²Π΅Ρ€Ρ…Π΅Π΄ ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½ΠΎ ~40% нСзависимо ΠΎΡ‚ Ρ€Π°Π·ΠΌΠ΅Ρ€Π° Ρ„Π°ΠΉΠ»Π°. Для Ρ‚ΠΈΠΏΠΈΡ‡Π½ΠΎΠ³ΠΎ Ρ„Π°ΠΉΠ»Π° Π½Π° 200 строк (~800 Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ²) hashline добавляСт ~600 Ρ‚ΠΎΠΊΠ΅Π½ΠΎΠ² β€” ΠΏΡ€Π΅Π½Π΅Π±Ρ€Π΅ΠΆΠΈΠΌΠΎ ΠΌΠ°Π»ΠΎ ΠΏΡ€ΠΈ контСкстном ΠΎΠΊΠ½Π΅ Π² 200K.
367
+
368
+ ### ΠŸΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΡŒ
369
+
370
+ | Π Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° | Аннотация | ΠŸΡ€Π°Π²ΠΊΠ° | Π£Π΄Π°Π»Π΅Π½ΠΈΠ΅ Ρ…Π΅ΡˆΠ΅ΠΉ |
371
+ |-------------:|:---------:|:------:|:--------------:|
372
+ | **10** строк | 0.05 мс | 0.01 мс | 0.03 мс |
373
+ | **100** строк | 0.12 мс | 0.02 мс | 0.08 мс |
374
+ | **1 000** строк | 0.95 мс | 0.04 мс | 0.60 мс |
375
+ | **5 000** строк | 4.50 мс | 0.08 мс | 2.80 мс |
376
+ | **10 000** строк | 9.20 мс | 0.10 мс | 5.50 мс |
377
+
378
+ > Π’ΠΈΠΏΠΈΡ‡Π½Ρ‹ΠΉ Ρ„Π°ΠΉΠ» ΠΈΠ· 1 000 строк аннотируСтся Π·Π° **< 1 мс** β€” Π½Π΅Π·Π°ΠΌΠ΅Ρ‚Π½ΠΎ для ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ.
379
+
380
+ ---
381
+
382
+ ## πŸ§‘β€πŸ’» Π Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠ°
383
+
384
+ ```bash
385
+ # Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ зависимости
386
+ npm install
387
+
388
+ # Π—Π°ΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ тСсты
389
+ npm test
390
+
391
+ # Π‘ΠΎΠ±Ρ€Π°Ρ‚ΡŒ
392
+ npm run build
393
+
394
+ # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Ρ‚ΠΈΠΏΠΎΠ²
395
+ npm run typecheck
396
+ ```
397
+
398
+ ---
399
+
400
+ ## πŸ’‘ Π’Π΄ΠΎΡ…Π½ΠΎΠ²Π΅Π½ΠΈΠ΅ ΠΈ тСорСтичСская Π±Π°Π·Π°
401
+
402
+ ИдСя hashline Π²Π΄ΠΎΡ…Π½ΠΎΠ²Π»Π΅Π½Π° концСпциями ΠΈΠ· **oh-my-pi** ΠΎΡ‚ [can1357](https://github.com/can1357/oh-my-pi) β€” AI-Ρ‚ΡƒΠ»ΠΊΠΈΡ‚Π° для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ (coding agent CLI, unified LLM API, TUI-Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ) β€” ΠΈ ΡΡ‚Π°Ρ‚ΡŒΠΈ Β«The Harness ProblemΒ» (ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ° обвязки).
403
+
404
+ **Π‘ΡƒΡ‚ΡŒ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹:** соврСмСнныС AI-ΠΌΠΎΠ΄Π΅Π»ΠΈ ΠΎΠ±Π»Π°Π΄Π°ΡŽΡ‚ ΠΎΠ³Ρ€ΠΎΠΌΠ½Ρ‹ΠΌΠΈ возмоТностями, Π½ΠΎ инструмСнты (harness), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΏΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‚ ΠΌΠΎΠ΄Π΅Π»ΠΈ контСкст ΠΈ ΠΏΡ€ΠΈΠΌΠ΅Π½ΡΡŽΡ‚ Π΅Ρ‘ ΠΏΡ€Π°Π²ΠΊΠΈ ΠΊ Ρ„Π°ΠΉΠ»Π°ΠΌ, Ρ‚Π΅Ρ€ΡΡŽΡ‚ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΈ ΠΏΠΎΡ€ΠΎΠΆΠ΄Π°ΡŽΡ‚ ошибки. МодСль Π²ΠΈΠ΄ΠΈΡ‚ содСрТимоС Ρ„Π°ΠΉΠ»Π°, Π½ΠΎ ΠΏΡ€ΠΈ Ρ€Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠΈ Π²Ρ‹Π½ΡƒΠΆΠ΄Π΅Π½Π° Β«ΡƒΠ³Π°Π΄Ρ‹Π²Π°Ρ‚ΡŒΒ» контСкст ΠΎΠΊΡ€ΡƒΠΆΠ°ΡŽΡ‰ΠΈΡ… строк. Search-and-replace ломаСтся Π½Π° Π΄ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚Π°Ρ… строк, Π° diff-Ρ„ΠΎΡ€ΠΌΠ°Ρ‚ Ρ‚ΠΎΠΆΠ΅ Π½Π΅Π½Π°Π΄Ρ‘ΠΆΠ΅Π½ Π½Π° ΠΏΡ€Π°ΠΊΡ‚ΠΈΠΊΠ΅.
405
+
406
+ Hashline Ρ€Π΅ΡˆΠ°Π΅Ρ‚ эту ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ, присваивая ΠΊΠ°ΠΆΠ΄ΠΎΠΉ строкС ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠΈΠΉ Π΄Π΅Ρ‚Π΅Ρ€ΠΌΠΈΠ½ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹ΠΉ Ρ…Π΅Ρˆ-Ρ‚Π΅Π³ (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, `2:f1c`), Ρ‡Ρ‚ΠΎ Π΄Π΅Π»Π°Π΅Ρ‚ Π°Π΄Ρ€Π΅ΡΠ°Ρ†ΠΈΡŽ строк **Ρ‚ΠΎΡ‡Π½ΠΎΠΉ ΠΈ ΠΎΠ΄Π½ΠΎΠ·Π½Π°Ρ‡Π½ΠΎΠΉ**. МодСль ΠΌΠΎΠΆΠ΅Ρ‚ ΡΡΡ‹Π»Π°Ρ‚ΡŒΡΡ Π½Π° Π»ΡŽΠ±ΡƒΡŽ строку ΠΈΠ»ΠΈ Π΄ΠΈΠ°ΠΏΠ°Π·ΠΎΠ½ Π±Π΅Π· ошибок смСщСния ΠΈ ΠΏΡƒΡ‚Π°Π½ΠΈΡ†Ρ‹ с Π΄ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚Π°ΠΌΠΈ.
407
+
408
+ **Бсылки:**
409
+ - [oh-my-pi ΠΎΡ‚ can1357](https://github.com/can1357/oh-my-pi) β€” AI-Ρ‚ΡƒΠ»ΠΊΠΈΡ‚ для Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ: coding agent CLI, unified LLM API, TUI-Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠΈ
410
+ - [The Harness Problem](https://blog.can.ac/2026/02/12/the-harness-problem/) β€” Π±Π»ΠΎΠ³-пост с ΠΏΠΎΠ΄Ρ€ΠΎΠ±Π½Ρ‹ΠΌ описаниСм ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹
411
+ - [Π‘Ρ‚Π°Ρ‚ΡŒΡ Π½Π° Π₯Π°Π±Ρ€Π΅](https://habr.com/ru/companies/bothub/news/995986/) β€” описаниС ΠΏΠΎΠ΄Ρ…ΠΎΠ΄Π° Π½Π° русском языкС
412
+
413
+ ---
414
+
415
+ ## πŸ“„ ЛицСнзия
416
+
417
+ [MIT](LICENSE) Β© opencode-hashline contributors
@@ -0,0 +1,177 @@
1
+ import {
2
+ formatFileWithHashes,
3
+ getByteLength,
4
+ resolveConfig,
5
+ shouldExclude,
6
+ stripHashes
7
+ } from "./chunk-IVZSANZ4.js";
8
+
9
+ // src/hooks.ts
10
+ import { appendFileSync } from "fs";
11
+ import { join } from "path";
12
+ import { homedir } from "os";
13
+ var DEBUG_LOG = join(homedir(), ".config", "opencode", "hashline-debug.log");
14
+ function debug(...args) {
15
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
16
+ `;
17
+ try {
18
+ appendFileSync(DEBUG_LOG, line);
19
+ } catch {
20
+ }
21
+ }
22
+ var FILE_READ_TOOLS = ["read", "file_read", "read_file", "cat", "view"];
23
+ var FILE_EDIT_TOOLS = ["write", "file_write", "file_edit", "edit", "edit_file", "patch", "apply_patch", "multiedit"];
24
+ function isFileReadTool(toolName, args) {
25
+ const lower = toolName.toLowerCase();
26
+ const nameMatch = FILE_READ_TOOLS.some(
27
+ (name) => lower === name || lower.endsWith(`.${name}`)
28
+ );
29
+ if (nameMatch) return true;
30
+ if (args && typeof args === "object") {
31
+ if (typeof args.path === "string" || typeof args.filePath === "string" || typeof args.file === "string") {
32
+ const writeIndicators = ["write", "edit", "patch", "execute", "run", "command", "shell", "bash"];
33
+ const isWrite = writeIndicators.some((w) => lower.includes(w));
34
+ if (!isWrite) return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
39
+ function createFileReadAfterHook(cache, config) {
40
+ const resolved = config ?? resolveConfig();
41
+ const hashLen = resolved.hashLength || 0;
42
+ const prefix = resolved.prefix;
43
+ return async (input, output) => {
44
+ debug("tool.execute.after:", input.tool, "args:", input.args);
45
+ if (!isFileReadTool(input.tool, input.args)) {
46
+ debug("skipped: not a file-read tool");
47
+ return;
48
+ }
49
+ if (!output.output || typeof output.output !== "string") {
50
+ debug("skipped: no string output, type:", typeof output.output, "keys:", Object.keys(output));
51
+ return;
52
+ }
53
+ const content = output.output;
54
+ if (resolved.maxFileSize > 0) {
55
+ const byteLength = getByteLength(content);
56
+ if (byteLength > resolved.maxFileSize) {
57
+ return;
58
+ }
59
+ }
60
+ const filePath = input.args?.path || input.args?.file || input.args?.filePath;
61
+ if (typeof filePath === "string" && shouldExclude(filePath, resolved.exclude)) {
62
+ return;
63
+ }
64
+ if (cache && typeof filePath === "string") {
65
+ const cached = cache.get(filePath, content);
66
+ if (cached) {
67
+ output.output = cached;
68
+ return;
69
+ }
70
+ }
71
+ const annotated = formatFileWithHashes(content, hashLen || void 0, prefix);
72
+ output.output = annotated;
73
+ debug("annotated", typeof filePath === "string" ? filePath : input.tool, "lines:", content.split("\n").length);
74
+ if (cache && typeof filePath === "string") {
75
+ cache.set(filePath, content, annotated);
76
+ }
77
+ };
78
+ }
79
+ function createFileEditBeforeHook(config) {
80
+ const resolved = config ?? resolveConfig();
81
+ const prefix = resolved.prefix;
82
+ return async (input, output) => {
83
+ const toolName = input.tool.toLowerCase();
84
+ const isFileEdit = FILE_EDIT_TOOLS.some(
85
+ (name) => toolName === name || toolName.endsWith(`.${name}`)
86
+ );
87
+ if (!isFileEdit) return;
88
+ if (!output.args || typeof output.args !== "object") return;
89
+ const contentFields = [
90
+ "content",
91
+ "new_content",
92
+ "old_content",
93
+ "old_string",
94
+ "new_string",
95
+ "replacement",
96
+ "text",
97
+ "diff",
98
+ "patch",
99
+ "patchText"
100
+ ];
101
+ for (const field of contentFields) {
102
+ if (typeof output.args[field] === "string") {
103
+ output.args[field] = stripHashes(output.args[field], prefix);
104
+ }
105
+ }
106
+ };
107
+ }
108
+ function createSystemPromptHook(config) {
109
+ const resolved = config ?? resolveConfig();
110
+ const prefix = resolved.prefix === false ? "" : resolved.prefix;
111
+ return async (_input, output) => {
112
+ output.system.push(
113
+ [
114
+ "## Hashline \u2014 Line Reference System",
115
+ "",
116
+ `File contents are annotated with hashline prefixes in the format \`${prefix}<line>:<hash>|<content>\`.`,
117
+ "The hash length adapts to file size: 3 chars for files \u22644096 lines, 4 chars for larger files.",
118
+ "",
119
+ "### Example (small file, 3-char hashes):",
120
+ "```",
121
+ `${prefix}1:a3f|function hello() {`,
122
+ `${prefix}2:f1c| return "world";`,
123
+ `${prefix}3:0e7|}`,
124
+ "```",
125
+ "",
126
+ "### Example (large file, 4-char hashes):",
127
+ "```",
128
+ `${prefix}1:a3f2|import { useState } from 'react';`,
129
+ `${prefix}2:f12c|`,
130
+ `${prefix}3:0e7a|export function App() {`,
131
+ "```",
132
+ "",
133
+ "### How to reference lines:",
134
+ "You can reference specific lines using their hash tags (e.g., `2:f1c` or `2:f12c`).",
135
+ "When editing files, you may include or omit the hash prefixes \u2014 they will be stripped automatically.",
136
+ "",
137
+ "### Edit operations using hash references:",
138
+ "",
139
+ "**Preferred tool-based edit (hash-aware):**",
140
+ '- Use the `hashline_edit` tool with refs like `startRef: "2:f1c"` and optional `endRef`.',
141
+ "- This avoids fragile old_string matching because edits are resolved by hash references.",
142
+ "",
143
+ "**Replace a single line:**",
144
+ '- "Replace line 2:f1c" \u2014 target a specific line unambiguously',
145
+ "",
146
+ "**Replace a block of lines:**",
147
+ '- "Replace block from 1:a3f to 3:0e7" \u2014 replace a range of lines',
148
+ "- Example: replace lines 1:a3f through 3:0e7 with new content",
149
+ "",
150
+ "**Insert content:**",
151
+ '- "Insert after 3:0e7" \u2014 insert new lines after a specific line',
152
+ '- "Insert before 1:a3f" \u2014 insert new lines before a specific line',
153
+ "",
154
+ "**Delete lines:**",
155
+ '- "Delete lines from 2:f1c to 3:0e7" \u2014 remove a range of lines',
156
+ "",
157
+ "### Hash verification rules:",
158
+ "- **Always verify** that the hash reference matches the current line content before editing.",
159
+ "- If a hash doesn't match, the file may have changed since you last read it \u2014 re-read the file first.",
160
+ '- Hash references include both the line number AND the content hash, so `2:f1c` means "line 2 with hash f1c".',
161
+ "- If you see a mismatch, do NOT proceed with the edit \u2014 re-read the file to get fresh references.",
162
+ "",
163
+ "### Best practices:",
164
+ "- Use hash references for all edit operations to ensure precision.",
165
+ "- When making multiple edits, work from bottom to top to avoid line number shifts.",
166
+ "- For large replacements, use range references (e.g., `1:a3f to 10:b2c`) instead of individual lines."
167
+ ].join("\n")
168
+ );
169
+ };
170
+ }
171
+
172
+ export {
173
+ isFileReadTool,
174
+ createFileReadAfterHook,
175
+ createFileEditBeforeHook,
176
+ createSystemPromptHook
177
+ };