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.
@@ -0,0 +1,411 @@
1
+ // src/hashline.ts
2
+ import picomatch from "picomatch";
3
+ var DEFAULT_EXCLUDE_PATTERNS = [
4
+ "**/node_modules/**",
5
+ "**/*.lock",
6
+ "**/package-lock.json",
7
+ "**/yarn.lock",
8
+ "**/pnpm-lock.yaml",
9
+ "**/*.min.js",
10
+ "**/*.min.css",
11
+ "**/*.bundle.js",
12
+ "**/*.map",
13
+ "**/*.wasm",
14
+ "**/*.png",
15
+ "**/*.jpg",
16
+ "**/*.jpeg",
17
+ "**/*.gif",
18
+ "**/*.ico",
19
+ "**/*.svg",
20
+ "**/*.woff",
21
+ "**/*.woff2",
22
+ "**/*.ttf",
23
+ "**/*.eot",
24
+ "**/*.pdf",
25
+ "**/*.zip",
26
+ "**/*.tar",
27
+ "**/*.gz",
28
+ "**/*.exe",
29
+ "**/*.dll",
30
+ "**/*.so",
31
+ "**/*.dylib"
32
+ ];
33
+ var DEFAULT_PREFIX = "#HL ";
34
+ var DEFAULT_CONFIG = {
35
+ exclude: DEFAULT_EXCLUDE_PATTERNS,
36
+ maxFileSize: 1048576,
37
+ // 1 MB
38
+ hashLength: 0,
39
+ // 0 = adaptive
40
+ cacheSize: 100,
41
+ prefix: DEFAULT_PREFIX
42
+ };
43
+ function resolveConfig(config, pluginConfig) {
44
+ const merged = {
45
+ ...pluginConfig,
46
+ ...config
47
+ };
48
+ if (!merged || Object.keys(merged).length === 0) {
49
+ return { ...DEFAULT_CONFIG, exclude: [...DEFAULT_CONFIG.exclude] };
50
+ }
51
+ return {
52
+ exclude: merged.exclude ?? [...DEFAULT_CONFIG.exclude],
53
+ maxFileSize: merged.maxFileSize ?? DEFAULT_CONFIG.maxFileSize,
54
+ hashLength: merged.hashLength ?? DEFAULT_CONFIG.hashLength,
55
+ cacheSize: merged.cacheSize ?? DEFAULT_CONFIG.cacheSize,
56
+ prefix: merged.prefix !== void 0 ? merged.prefix : DEFAULT_CONFIG.prefix
57
+ };
58
+ }
59
+ function fnv1aHash(str) {
60
+ let hash = 2166136261;
61
+ for (let i = 0; i < str.length; i++) {
62
+ hash ^= str.charCodeAt(i);
63
+ hash = hash * 16777619 >>> 0;
64
+ }
65
+ return hash;
66
+ }
67
+ var modulusCache = /* @__PURE__ */ new Map();
68
+ function getModulus(hashLen) {
69
+ let cached = modulusCache.get(hashLen);
70
+ if (cached === void 0) {
71
+ cached = Math.pow(16, hashLen);
72
+ modulusCache.set(hashLen, cached);
73
+ }
74
+ return cached;
75
+ }
76
+ function getAdaptiveHashLength(lineCount) {
77
+ if (lineCount <= 4096) return 3;
78
+ return 4;
79
+ }
80
+ function computeLineHash(idx, line, hashLen = 3) {
81
+ const trimmed = line.trimEnd();
82
+ const input = `${idx}:${trimmed}`;
83
+ const raw = fnv1aHash(input);
84
+ const modulus = getModulus(hashLen);
85
+ const hash = raw % modulus;
86
+ return hash.toString(16).padStart(hashLen, "0");
87
+ }
88
+ function formatFileWithHashes(content, hashLen, prefix) {
89
+ const lines = content.split("\n");
90
+ const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
91
+ const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
92
+ const hashes = new Array(lines.length);
93
+ const seen = /* @__PURE__ */ new Map();
94
+ for (let idx = 0; idx < lines.length; idx++) {
95
+ const hash = computeLineHash(idx, lines[idx], effectiveLen);
96
+ if (seen.has(hash)) {
97
+ const longerLen = Math.min(effectiveLen + 1, 8);
98
+ hashes[idx] = computeLineHash(idx, lines[idx], longerLen);
99
+ } else {
100
+ seen.set(hash, idx);
101
+ hashes[idx] = hash;
102
+ }
103
+ }
104
+ return lines.map((line, idx) => {
105
+ return `${effectivePrefix}${idx + 1}:${hashes[idx]}|${line}`;
106
+ }).join("\n");
107
+ }
108
+ var stripRegexCache = /* @__PURE__ */ new Map();
109
+ function stripHashes(content, prefix) {
110
+ const effectivePrefix = prefix === void 0 ? DEFAULT_PREFIX : prefix === false ? "" : prefix;
111
+ const escapedPrefix = effectivePrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
112
+ let hashLinePattern = stripRegexCache.get(escapedPrefix);
113
+ if (!hashLinePattern) {
114
+ hashLinePattern = new RegExp(`^${escapedPrefix}\\d+:[0-9a-f]{2,8}\\|`);
115
+ stripRegexCache.set(escapedPrefix, hashLinePattern);
116
+ }
117
+ return content.split("\n").map((line) => {
118
+ const match = line.match(hashLinePattern);
119
+ if (match) {
120
+ return line.slice(match[0].length);
121
+ }
122
+ return line;
123
+ }).join("\n");
124
+ }
125
+ function parseHashRef(ref) {
126
+ const match = ref.match(/^(\d+):([0-9a-f]{2,8})$/);
127
+ if (!match) {
128
+ throw new Error(`Invalid hash reference: "${ref}". Expected format: "<line>:<2-8 char hex>"`);
129
+ }
130
+ return {
131
+ line: parseInt(match[1], 10),
132
+ hash: match[2]
133
+ };
134
+ }
135
+ function normalizeHashRef(ref) {
136
+ const trimmed = ref.trim();
137
+ const plain = trimmed.match(/^(\d+):([0-9a-f]{2,8})$/i);
138
+ if (plain) {
139
+ return `${parseInt(plain[1], 10)}:${plain[2].toLowerCase()}`;
140
+ }
141
+ const annotated = trimmed.match(/^(?:[#\w]*\s+)?(\d+):([0-9a-f]{2,8})\|.*$/i);
142
+ if (annotated) {
143
+ return `${parseInt(annotated[1], 10)}:${annotated[2].toLowerCase()}`;
144
+ }
145
+ throw new Error(
146
+ `Invalid hash reference: "${ref}". Expected "<line>:<hash>" or an annotated line like "#HL <line>:<hash>|..."`
147
+ );
148
+ }
149
+ function buildHashMap(content, hashLen) {
150
+ const lines = content.split("\n");
151
+ const effectiveLen = hashLen && hashLen >= 3 ? hashLen : getAdaptiveHashLength(lines.length);
152
+ const map = /* @__PURE__ */ new Map();
153
+ for (let idx = 0; idx < lines.length; idx++) {
154
+ const hash = computeLineHash(idx, lines[idx], effectiveLen);
155
+ const lineNum = idx + 1;
156
+ map.set(`${lineNum}:${hash}`, lineNum);
157
+ }
158
+ return map;
159
+ }
160
+ function verifyHash(lineNumber, hash, currentContent, hashLen, lines) {
161
+ const contentLines = lines ?? currentContent.split("\n");
162
+ const effectiveLen = hashLen && hashLen >= 2 ? hashLen : hash.length;
163
+ if (lineNumber < 1 || lineNumber > contentLines.length) {
164
+ return {
165
+ valid: false,
166
+ message: `Line ${lineNumber} is out of range (file has ${contentLines.length} lines)`
167
+ };
168
+ }
169
+ const idx = lineNumber - 1;
170
+ const actualHash = computeLineHash(idx, contentLines[idx], effectiveLen);
171
+ if (actualHash !== hash) {
172
+ return {
173
+ valid: false,
174
+ expected: hash,
175
+ actual: actualHash,
176
+ message: `Hash mismatch at line ${lineNumber}: expected "${hash}", got "${actualHash}". The file may have changed since it was read.`
177
+ };
178
+ }
179
+ return { valid: true };
180
+ }
181
+ function resolveRange(startRef, endRef, content, hashLen) {
182
+ const start = parseHashRef(startRef);
183
+ const end = parseHashRef(endRef);
184
+ if (start.line > end.line) {
185
+ throw new Error(
186
+ `Invalid range: start line ${start.line} is after end line ${end.line}`
187
+ );
188
+ }
189
+ const lines = content.split("\n");
190
+ const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
191
+ if (!startVerify.valid) {
192
+ throw new Error(`Start reference invalid: ${startVerify.message}`);
193
+ }
194
+ const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
195
+ if (!endVerify.valid) {
196
+ throw new Error(`End reference invalid: ${endVerify.message}`);
197
+ }
198
+ const rangeLines = lines.slice(start.line - 1, end.line);
199
+ return {
200
+ startLine: start.line,
201
+ endLine: end.line,
202
+ lines: rangeLines,
203
+ content: rangeLines.join("\n")
204
+ };
205
+ }
206
+ function replaceRange(startRef, endRef, content, replacement, hashLen) {
207
+ const range = resolveRange(startRef, endRef, content, hashLen);
208
+ const lines = content.split("\n");
209
+ const before = lines.slice(0, range.startLine - 1);
210
+ const after = lines.slice(range.endLine);
211
+ const replacementLines = replacement.split("\n");
212
+ return [...before, ...replacementLines, ...after].join("\n");
213
+ }
214
+ function applyHashEdit(input, content, hashLen) {
215
+ const normalizedStart = normalizeHashRef(input.startRef);
216
+ const start = parseHashRef(normalizedStart);
217
+ const lines = content.split("\n");
218
+ const startVerify = verifyHash(start.line, start.hash, content, hashLen, lines);
219
+ if (!startVerify.valid) {
220
+ throw new Error(`Start reference invalid: ${startVerify.message}`);
221
+ }
222
+ if (input.operation === "insert_before" || input.operation === "insert_after") {
223
+ if (input.replacement === void 0) {
224
+ throw new Error(`Operation "${input.operation}" requires "replacement" content`);
225
+ }
226
+ const insertionLines = input.replacement.split("\n");
227
+ const insertIndex = input.operation === "insert_before" ? start.line - 1 : start.line;
228
+ const next2 = [
229
+ ...lines.slice(0, insertIndex),
230
+ ...insertionLines,
231
+ ...lines.slice(insertIndex)
232
+ ].join("\n");
233
+ return {
234
+ operation: input.operation,
235
+ startLine: start.line,
236
+ endLine: start.line,
237
+ content: next2
238
+ };
239
+ }
240
+ const normalizedEnd = normalizeHashRef(input.endRef ?? input.startRef);
241
+ const end = parseHashRef(normalizedEnd);
242
+ if (start.line > end.line) {
243
+ throw new Error(
244
+ `Invalid range: start line ${start.line} is after end line ${end.line}`
245
+ );
246
+ }
247
+ const endVerify = verifyHash(end.line, end.hash, content, hashLen, lines);
248
+ if (!endVerify.valid) {
249
+ throw new Error(`End reference invalid: ${endVerify.message}`);
250
+ }
251
+ const replacement = input.operation === "delete" ? "" : input.replacement;
252
+ if (replacement === void 0) {
253
+ throw new Error(`Operation "${input.operation}" requires "replacement" content`);
254
+ }
255
+ const before = lines.slice(0, start.line - 1);
256
+ const after = lines.slice(end.line);
257
+ const replacementLines = input.operation === "delete" ? [] : replacement.split("\n");
258
+ const next = [...before, ...replacementLines, ...after].join("\n");
259
+ return {
260
+ operation: input.operation,
261
+ startLine: start.line,
262
+ endLine: end.line,
263
+ content: next
264
+ };
265
+ }
266
+ var HashlineCache = class {
267
+ cache;
268
+ maxSize;
269
+ constructor(maxSize = 100) {
270
+ this.cache = /* @__PURE__ */ new Map();
271
+ this.maxSize = maxSize;
272
+ }
273
+ /**
274
+ * Get cached annotated content for a file, or null if not cached / stale.
275
+ */
276
+ get(filePath, content) {
277
+ const entry = this.cache.get(filePath);
278
+ if (!entry) return null;
279
+ const currentHash = fnv1aHash(content);
280
+ if (entry.contentHash !== currentHash) {
281
+ this.cache.delete(filePath);
282
+ return null;
283
+ }
284
+ this.cache.delete(filePath);
285
+ this.cache.set(filePath, entry);
286
+ return entry.annotated;
287
+ }
288
+ /**
289
+ * Store annotated content in the cache.
290
+ */
291
+ set(filePath, content, annotated) {
292
+ if (this.cache.has(filePath)) {
293
+ this.cache.delete(filePath);
294
+ }
295
+ if (this.cache.size >= this.maxSize) {
296
+ const oldest = this.cache.keys().next().value;
297
+ if (oldest !== void 0) {
298
+ this.cache.delete(oldest);
299
+ }
300
+ }
301
+ this.cache.set(filePath, {
302
+ contentHash: fnv1aHash(content),
303
+ annotated
304
+ });
305
+ }
306
+ /**
307
+ * Invalidate a specific file from the cache.
308
+ */
309
+ invalidate(filePath) {
310
+ this.cache.delete(filePath);
311
+ }
312
+ /**
313
+ * Clear the entire cache.
314
+ */
315
+ clear() {
316
+ this.cache.clear();
317
+ }
318
+ /**
319
+ * Get the current number of cached entries.
320
+ */
321
+ get size() {
322
+ return this.cache.size;
323
+ }
324
+ };
325
+ function matchesGlob(filePath, pattern) {
326
+ const normalizedPath = filePath.replace(/\\/g, "/");
327
+ const normalizedPattern = pattern.replace(/\\/g, "/");
328
+ const isMatch = picomatch(normalizedPattern, { dot: true });
329
+ return isMatch(normalizedPath);
330
+ }
331
+ function shouldExclude(filePath, patterns) {
332
+ return patterns.some((pattern) => matchesGlob(filePath, pattern));
333
+ }
334
+ var textEncoder = new TextEncoder();
335
+ function getByteLength(content) {
336
+ return textEncoder.encode(content).length;
337
+ }
338
+ function createHashline(config) {
339
+ const resolved = resolveConfig(config);
340
+ const cache = new HashlineCache(resolved.cacheSize);
341
+ const hl = resolved.hashLength || 0;
342
+ const pfx = resolved.prefix;
343
+ return {
344
+ config: resolved,
345
+ cache,
346
+ formatFileWithHashes(content, filePath) {
347
+ if (filePath) {
348
+ const cached = cache.get(filePath, content);
349
+ if (cached) return cached;
350
+ }
351
+ const result = formatFileWithHashes(content, hl, pfx);
352
+ if (filePath) {
353
+ cache.set(filePath, content, result);
354
+ }
355
+ return result;
356
+ },
357
+ stripHashes(content) {
358
+ return stripHashes(content, pfx);
359
+ },
360
+ computeLineHash(idx, line) {
361
+ return computeLineHash(idx, line, hl || 3);
362
+ },
363
+ buildHashMap(content) {
364
+ return buildHashMap(content, hl);
365
+ },
366
+ verifyHash(lineNumber, hash, currentContent) {
367
+ return verifyHash(lineNumber, hash, currentContent, hl);
368
+ },
369
+ resolveRange(startRef, endRef, content) {
370
+ return resolveRange(startRef, endRef, content, hl);
371
+ },
372
+ replaceRange(startRef, endRef, content, replacement) {
373
+ return replaceRange(startRef, endRef, content, replacement, hl);
374
+ },
375
+ applyHashEdit(input, content) {
376
+ return applyHashEdit(input, content, hl);
377
+ },
378
+ normalizeHashRef(ref) {
379
+ return normalizeHashRef(ref);
380
+ },
381
+ parseHashRef(ref) {
382
+ return parseHashRef(ref);
383
+ },
384
+ shouldExclude(filePath) {
385
+ return shouldExclude(filePath, resolved.exclude);
386
+ }
387
+ };
388
+ }
389
+
390
+ export {
391
+ DEFAULT_EXCLUDE_PATTERNS,
392
+ DEFAULT_PREFIX,
393
+ DEFAULT_CONFIG,
394
+ resolveConfig,
395
+ getAdaptiveHashLength,
396
+ computeLineHash,
397
+ formatFileWithHashes,
398
+ stripHashes,
399
+ parseHashRef,
400
+ normalizeHashRef,
401
+ buildHashMap,
402
+ verifyHash,
403
+ resolveRange,
404
+ replaceRange,
405
+ applyHashEdit,
406
+ HashlineCache,
407
+ matchesGlob,
408
+ shouldExclude,
409
+ getByteLength,
410
+ createHashline
411
+ };
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Core Hashline logic — content-addressable line hashing for precise AI code editing.
3
+ *
4
+ * Each line gets a hex hash tag derived from its index and trimmed content.
5
+ * Hash length adapts to file size: 3 chars (≤4096 lines), 4 chars (>4096).
6
+ * Minimum hash length is 3 to reduce collision risk.
7
+ * Format: `#HL <lineNumber>:<hash>|<originalLine>`
8
+ *
9
+ * Example:
10
+ * #HL 1:a3f|function hello() {
11
+ * #HL 2:f1c| return "world";
12
+ * #HL 3:0e7|}
13
+ */
14
+ /**
15
+ * Configuration object for Hashline.
16
+ */
17
+ interface HashlineConfig {
18
+ /** Glob patterns to exclude from processing */
19
+ exclude?: string[];
20
+ /** Maximum file size in bytes to process (default: 1MB) */
21
+ maxFileSize?: number;
22
+ /** Override hash length (3–4). If not set, adaptive length is used. */
23
+ hashLength?: number;
24
+ /** LRU cache size — number of files to cache (default: 100) */
25
+ cacheSize?: number;
26
+ /**
27
+ * Magic prefix for hashline annotations.
28
+ * Default: "#HL " — lines are formatted as `#HL 1:a3f|code here`.
29
+ * Set to `false` to disable prefix (legacy format: `1:a3f|code here`).
30
+ */
31
+ prefix?: string | false;
32
+ }
33
+ /** Default exclude patterns */
34
+ declare const DEFAULT_EXCLUDE_PATTERNS: string[];
35
+ /** Default prefix for hashline annotations */
36
+ declare const DEFAULT_PREFIX = "#HL ";
37
+ /** Default configuration values */
38
+ declare const DEFAULT_CONFIG: Required<HashlineConfig>;
39
+ /**
40
+ * Merge user config with defaults.
41
+ *
42
+ * @param config - optional partial user config
43
+ * @param pluginConfig - optional config from plugin context (e.g. opencode.json)
44
+ */
45
+ declare function resolveConfig(config?: HashlineConfig, pluginConfig?: HashlineConfig): Required<HashlineConfig>;
46
+ /**
47
+ * Determine the appropriate hash length based on the number of lines.
48
+ *
49
+ * Minimum hash length is 3 to reduce collision risk.
50
+ * - ≤4096 lines → 3 hex chars (4096 values)
51
+ * - >4096 lines → 4 hex chars (65536 values)
52
+ */
53
+ declare function getAdaptiveHashLength(lineCount: number): number;
54
+ /**
55
+ * Compute a hex hash for a given line.
56
+ *
57
+ * Uses trimEnd() so that leading whitespace (indentation) IS significant,
58
+ * but trailing whitespace is ignored.
59
+ *
60
+ * @param idx - 0-based line index
61
+ * @param line - the raw line content
62
+ * @param hashLen - number of hex characters (3–4, default 3)
63
+ * @returns lowercase hex string of the specified length
64
+ */
65
+ declare function computeLineHash(idx: number, line: string, hashLen?: number): string;
66
+ /**
67
+ * Format file content with hashline annotations.
68
+ *
69
+ * Each line becomes: `<prefix><1-based lineNumber>:<hash>|<originalLine>`
70
+ * Hash length adapts to file size unless overridden.
71
+ * Includes collision detection: if two lines produce the same hash,
72
+ * the colliding line gets a longer hash.
73
+ *
74
+ * @param content - raw file content
75
+ * @param hashLen - override hash length (0 or undefined = adaptive)
76
+ * @param prefix - prefix string (default "#HL "), or false to disable
77
+ * @returns annotated content with hash prefixes
78
+ */
79
+ declare function formatFileWithHashes(content: string, hashLen?: number, prefix?: string | false): string;
80
+ /**
81
+ * Strip hashline prefixes to recover original file content.
82
+ *
83
+ * Recognizes the pattern `<prefix><number>:<2-8 hex>|` at the start of each line.
84
+ * By default looks for the `#HL ` prefix to avoid false positives.
85
+ *
86
+ * @param content - hashline-annotated content
87
+ * @param prefix - prefix string (default "#HL "), or false for legacy format
88
+ * @returns original content without hash prefixes
89
+ */
90
+ declare function stripHashes(content: string, prefix?: string | false): string;
91
+ /**
92
+ * Parse a hash reference like "2:f1a" or "2:f1a3" into its components.
93
+ *
94
+ * @param ref - reference string in the format "<lineNumber>:<hash>"
95
+ * @returns parsed line number (1-based) and hash string
96
+ */
97
+ declare function parseHashRef(ref: string): {
98
+ line: number;
99
+ hash: string;
100
+ };
101
+ /**
102
+ * Normalize a hash reference.
103
+ *
104
+ * Accepts:
105
+ * - plain refs: `2:f1c`
106
+ * - annotated refs: `#HL 2:f1c|line content` or `2:f1c|line content`
107
+ *
108
+ * Returns canonical lowercased format: `<line>:<hash>`
109
+ */
110
+ declare function normalizeHashRef(ref: string): string;
111
+ /**
112
+ * Supported hash-aware edit operations.
113
+ */
114
+ type HashEditOperation = "replace" | "delete" | "insert_before" | "insert_after";
115
+ /**
116
+ * Input for hash-aware edit application.
117
+ */
118
+ interface HashEditInput {
119
+ operation: HashEditOperation;
120
+ startRef: string;
121
+ endRef?: string;
122
+ replacement?: string;
123
+ }
124
+ /**
125
+ * Result of applying a hash-aware edit.
126
+ */
127
+ interface HashEditResult {
128
+ operation: HashEditOperation;
129
+ startLine: number;
130
+ endLine: number;
131
+ content: string;
132
+ }
133
+ /**
134
+ * Build a mapping from hash tags to 1-based line numbers.
135
+ *
136
+ * Note: If multiple lines produce the same hash, the last one wins.
137
+ * In practice, collisions are rare since the hash incorporates the line index.
138
+ *
139
+ * @param content - raw file content (without hash prefixes)
140
+ * @param hashLen - override hash length (0 or undefined = adaptive)
141
+ * @returns Map from "<lineNumber>:<hash>" to 1-based line number
142
+ */
143
+ declare function buildHashMap(content: string, hashLen?: number): Map<string, number>;
144
+ /**
145
+ * Result of hash verification.
146
+ */
147
+ interface VerifyHashResult {
148
+ valid: boolean;
149
+ expected?: string;
150
+ actual?: string;
151
+ message?: string;
152
+ }
153
+ /**
154
+ * Verify that a line's hash matches the current content.
155
+ *
156
+ * This protects against race conditions — if the file changed between
157
+ * read and edit, the hash won't match.
158
+ *
159
+ * The hash length is determined from the provided hash string itself
160
+ * (hash.length), not from the current file size. This ensures that
161
+ * a reference like "2:f1a" remains valid even if the file has grown.
162
+ *
163
+ * @param lineNumber - 1-based line number
164
+ * @param hash - expected hash from the hash reference
165
+ * @param currentContent - current raw file content (string or pre-split lines)
166
+ * @param hashLen - override hash length (0 or undefined = use hash.length from ref)
167
+ * @param lines - optional pre-split lines array to avoid re-splitting
168
+ * @returns verification result
169
+ */
170
+ declare function verifyHash(lineNumber: number, hash: string, currentContent: string, hashLen?: number, lines?: string[]): VerifyHashResult;
171
+ /**
172
+ * Result of a range resolution.
173
+ */
174
+ interface ResolvedRange {
175
+ startLine: number;
176
+ endLine: number;
177
+ lines: string[];
178
+ content: string;
179
+ }
180
+ /**
181
+ * Resolve a range of lines by hash references.
182
+ * Splits content once and passes lines array to verifyHash to avoid redundant splits.
183
+ *
184
+ * @param startRef - start hash reference (e.g. "1:a3f")
185
+ * @param endRef - end hash reference (e.g. "3:0e7")
186
+ * @param content - raw file content
187
+ * @param hashLen - override hash length (0 or undefined = use hash.length from ref)
188
+ * @returns resolved range with line numbers and content
189
+ */
190
+ declare function resolveRange(startRef: string, endRef: string, content: string, hashLen?: number): ResolvedRange;
191
+ /**
192
+ * Replace a range of lines identified by hash references with new content.
193
+ * Splits content once and reuses the lines array.
194
+ *
195
+ * @param startRef - start hash reference
196
+ * @param endRef - end hash reference
197
+ * @param content - current raw file content
198
+ * @param replacement - new content to replace the range with
199
+ * @param hashLen - override hash length (0 or undefined = use hash.length from ref)
200
+ * @returns new file content with the range replaced
201
+ */
202
+ declare function replaceRange(startRef: string, endRef: string, content: string, replacement: string, hashLen?: number): string;
203
+ /**
204
+ * Apply a hash-aware edit operation directly against file content.
205
+ *
206
+ * Unlike search/replace tools, this resolves references by line+hash and
207
+ * verifies them before editing, so exact old-string matching is not required.
208
+ */
209
+ declare function applyHashEdit(input: HashEditInput, content: string, hashLen?: number): HashEditResult;
210
+ /**
211
+ * Simple LRU cache for annotated file content.
212
+ */
213
+ declare class HashlineCache {
214
+ private cache;
215
+ private maxSize;
216
+ constructor(maxSize?: number);
217
+ /**
218
+ * Get cached annotated content for a file, or null if not cached / stale.
219
+ */
220
+ get(filePath: string, content: string): string | null;
221
+ /**
222
+ * Store annotated content in the cache.
223
+ */
224
+ set(filePath: string, content: string, annotated: string): void;
225
+ /**
226
+ * Invalidate a specific file from the cache.
227
+ */
228
+ invalidate(filePath: string): void;
229
+ /**
230
+ * Clear the entire cache.
231
+ */
232
+ clear(): void;
233
+ /**
234
+ * Get the current number of cached entries.
235
+ */
236
+ get size(): number;
237
+ }
238
+ /**
239
+ * Glob matcher using picomatch for full glob support.
240
+ * Supports `*`, `**`, `?`, `{a,b}`, `[abc]`, and all standard glob patterns.
241
+ * Windows paths are normalized to forward slashes.
242
+ */
243
+ declare function matchesGlob(filePath: string, pattern: string): boolean;
244
+ /**
245
+ * Check if a file path should be excluded based on config patterns.
246
+ */
247
+ declare function shouldExclude(filePath: string, patterns: string[]): boolean;
248
+ declare function getByteLength(content: string): number;
249
+ /**
250
+ * A Hashline instance with custom configuration.
251
+ */
252
+ interface HashlineInstance {
253
+ config: Required<HashlineConfig>;
254
+ cache: HashlineCache;
255
+ formatFileWithHashes: (content: string, filePath?: string) => string;
256
+ stripHashes: (content: string) => string;
257
+ computeLineHash: (idx: number, line: string) => string;
258
+ buildHashMap: (content: string) => Map<string, number>;
259
+ verifyHash: (lineNumber: number, hash: string, currentContent: string) => VerifyHashResult;
260
+ resolveRange: (startRef: string, endRef: string, content: string) => ResolvedRange;
261
+ replaceRange: (startRef: string, endRef: string, content: string, replacement: string) => string;
262
+ applyHashEdit: (input: HashEditInput, content: string) => HashEditResult;
263
+ normalizeHashRef: (ref: string) => string;
264
+ parseHashRef: (ref: string) => {
265
+ line: number;
266
+ hash: string;
267
+ };
268
+ shouldExclude: (filePath: string) => boolean;
269
+ }
270
+ /**
271
+ * Create a Hashline instance with custom configuration.
272
+ *
273
+ * @param config - custom configuration options
274
+ * @returns configured Hashline instance
275
+ */
276
+ declare function createHashline(config?: HashlineConfig): HashlineInstance;
277
+
278
+ export { DEFAULT_CONFIG as D, type HashlineConfig as H, type ResolvedRange as R, type VerifyHashResult as V, type HashEditInput as a, type HashEditOperation as b, type HashEditResult as c, type HashlineInstance as d, HashlineCache as e, DEFAULT_EXCLUDE_PATTERNS as f, DEFAULT_PREFIX as g, applyHashEdit as h, buildHashMap as i, computeLineHash as j, createHashline as k, formatFileWithHashes as l, getAdaptiveHashLength as m, getByteLength as n, matchesGlob as o, normalizeHashRef as p, parseHashRef as q, replaceRange as r, resolveConfig as s, resolveRange as t, shouldExclude as u, stripHashes as v, verifyHash as w };