@oh-my-pi/pi-coding-agent 15.5.3 → 15.5.4

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/config/settings-schema.d.ts +27 -0
  3. package/dist/types/config.d.ts +31 -5
  4. package/dist/types/edit/file-snapshot-store.d.ts +18 -0
  5. package/dist/types/edit/hashline/diff.d.ts +30 -0
  6. package/dist/types/edit/hashline/execute.d.ts +29 -0
  7. package/dist/types/edit/hashline/filesystem.d.ts +57 -0
  8. package/dist/types/edit/hashline/index.d.ts +4 -0
  9. package/dist/types/edit/hashline/params.d.ts +12 -0
  10. package/dist/types/edit/index.d.ts +4 -3
  11. package/dist/types/edit/normalize.d.ts +4 -16
  12. package/dist/types/index.d.ts +0 -1
  13. package/dist/types/tools/index.d.ts +6 -5
  14. package/dist/types/tools/path-utils.d.ts +18 -0
  15. package/dist/types/utils/changelog.d.ts +8 -3
  16. package/package.json +8 -15
  17. package/src/config/settings-schema.ts +32 -0
  18. package/src/config.ts +42 -15
  19. package/src/edit/file-snapshot-store.ts +22 -0
  20. package/src/edit/hashline/diff.ts +88 -0
  21. package/src/edit/hashline/execute.ts +188 -0
  22. package/src/edit/hashline/filesystem.ts +129 -0
  23. package/src/edit/hashline/index.ts +4 -0
  24. package/src/edit/hashline/params.ts +11 -0
  25. package/src/edit/index.ts +7 -15
  26. package/src/edit/normalize.ts +11 -41
  27. package/src/edit/renderer.ts +1 -1
  28. package/src/edit/streaming.ts +8 -9
  29. package/src/index.ts +0 -1
  30. package/src/internal-urls/docs-index.generated.ts +1 -1
  31. package/src/sdk.ts +8 -1
  32. package/src/tools/ast-edit.ts +1 -1
  33. package/src/tools/ast-grep.ts +3 -3
  34. package/src/tools/index.ts +6 -5
  35. package/src/tools/path-utils.ts +81 -0
  36. package/src/tools/read.ts +14 -72
  37. package/src/tools/search.ts +136 -17
  38. package/src/tools/write.ts +3 -3
  39. package/src/utils/changelog.ts +11 -3
  40. package/src/utils/file-mentions.ts +1 -1
  41. package/dist/types/edit/file-read-cache.d.ts +0 -36
  42. package/dist/types/hashline/anchors.d.ts +0 -26
  43. package/dist/types/hashline/apply.d.ts +0 -14
  44. package/dist/types/hashline/constants.d.ts +0 -48
  45. package/dist/types/hashline/diff-preview.d.ts +0 -2
  46. package/dist/types/hashline/diff.d.ts +0 -16
  47. package/dist/types/hashline/execute.d.ts +0 -4
  48. package/dist/types/hashline/executor.d.ts +0 -56
  49. package/dist/types/hashline/hash.d.ts +0 -76
  50. package/dist/types/hashline/index.d.ts +0 -14
  51. package/dist/types/hashline/input.d.ts +0 -4
  52. package/dist/types/hashline/prefixes.d.ts +0 -7
  53. package/dist/types/hashline/recovery.d.ts +0 -21
  54. package/dist/types/hashline/stream.d.ts +0 -2
  55. package/dist/types/hashline/tokenizer.d.ts +0 -94
  56. package/dist/types/hashline/types.d.ts +0 -75
  57. package/src/edit/file-read-cache.ts +0 -138
  58. package/src/hashline/anchors.ts +0 -104
  59. package/src/hashline/apply.ts +0 -790
  60. package/src/hashline/bigrams.json +0 -649
  61. package/src/hashline/constants.ts +0 -60
  62. package/src/hashline/diff-preview.ts +0 -42
  63. package/src/hashline/diff.ts +0 -82
  64. package/src/hashline/execute.ts +0 -334
  65. package/src/hashline/executor.ts +0 -347
  66. package/src/hashline/grammar.lark +0 -22
  67. package/src/hashline/hash.ts +0 -131
  68. package/src/hashline/index.ts +0 -14
  69. package/src/hashline/input.ts +0 -137
  70. package/src/hashline/prefixes.ts +0 -111
  71. package/src/hashline/recovery.ts +0 -139
  72. package/src/hashline/stream.ts +0 -123
  73. package/src/hashline/tokenizer.ts +0 -473
  74. package/src/hashline/types.ts +0 -66
  75. package/src/prompts/tools/hashline.md +0 -83
@@ -1,473 +0,0 @@
1
- import { ABORT_MARKER, BEGIN_PATCH_MARKER, END_PATCH_MARKER } from "./constants";
2
- import {
3
- describeAnchorExamples,
4
- HL_FILE_HASH_SEP,
5
- HL_FILE_PREFIX,
6
- HL_OP_DELETE,
7
- HL_OP_INSERT_AFTER,
8
- HL_OP_INSERT_BEFORE,
9
- HL_OP_REPLACE,
10
- HL_PAYLOAD_PREFIX,
11
- } from "./hash";
12
- import type { Anchor, HashlineCursor } from "./types";
13
-
14
- const CHAR_LINE_FEED = 10;
15
- const CHAR_CARRIAGE_RETURN = 13;
16
- const CHAR_ZERO = 48;
17
- const CHAR_NINE = 57;
18
- const CHAR_HASH = 35;
19
- const CHAR_TAB = 9;
20
- const CHAR_SPACE = 32;
21
- const CHAR_LOWER_A = 97;
22
- const CHAR_LOWER_F = 102;
23
- const CHAR_PILCROW = HL_FILE_PREFIX.charCodeAt(0);
24
- const CHAR_PAYLOAD_PREFIX = HL_PAYLOAD_PREFIX.charCodeAt(0);
25
- const FILE_HASH_LENGTH = 4;
26
-
27
- function isDigitCode(code: number): boolean {
28
- return code >= CHAR_ZERO && code <= CHAR_NINE;
29
- }
30
-
31
- function isNonZeroDigitCode(code: number): boolean {
32
- return code > CHAR_ZERO && code <= CHAR_NINE;
33
- }
34
-
35
- function isDecorationCode(code: number): boolean {
36
- return code === 42 || code === 45 || code === 62;
37
- }
38
-
39
- function isHexDigitCode(code: number): boolean {
40
- return isDigitCode(code) || (code >= CHAR_LOWER_A && code <= CHAR_LOWER_F);
41
- }
42
-
43
- function skipWhitespace(line: string, index: number, end = line.length): number {
44
- return end - line.slice(index, end).trimStart().length;
45
- }
46
-
47
- function trimEndIndex(line: string): number {
48
- return line.trimEnd().length;
49
- }
50
-
51
- function isEmptyLine(line: string): boolean {
52
- return line.length === 0;
53
- }
54
-
55
- function markerLineEquals(line: string, marker: string): boolean {
56
- return line.trimEnd() === marker;
57
- }
58
-
59
- /**
60
- * Split a hashline diff into individual lines without losing the trailing
61
- * empty line that callers may rely on for explicit blank payloads. CRLF pairs
62
- * are normalized to a single line break.
63
- *
64
- * This mirrors the line-splitting performed by {@link HashlineTokenizer}'s
65
- * streaming drain loop and is kept for non-streaming callers that prefer
66
- * a single-shot split.
67
- */
68
- export function splitHashlineLines(text: string): string[] {
69
- if (text.length === 0) return [""];
70
-
71
- const lines: string[] = [];
72
- let start = 0;
73
- for (let index = 0; index < text.length; index++) {
74
- if (text.charCodeAt(index) !== CHAR_LINE_FEED) continue;
75
- let end = index;
76
- if (end > start && text.charCodeAt(end - 1) === CHAR_CARRIAGE_RETURN) end--;
77
- lines.push(text.slice(start, end));
78
- start = index + 1;
79
- }
80
-
81
- if (start < text.length) {
82
- let end = text.length;
83
- if (end > start && text.charCodeAt(end - 1) === CHAR_CARRIAGE_RETURN) end--;
84
- lines.push(text.slice(start, end));
85
- }
86
- return lines;
87
- }
88
-
89
- export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
90
- if (cursor.kind === "before_anchor") return { kind: "before_anchor", anchor: { ...cursor.anchor } };
91
- if (cursor.kind === "after_anchor") return { kind: "after_anchor", anchor: { ...cursor.anchor } };
92
- return cursor;
93
- }
94
-
95
- // Leniently accept anchors copied from read/search output:
96
- // - optional leading line-marker decoration (`*`, `>`, `-`)
97
- // - the required bare line number
98
- function skipDecoratedAnchorPrefix(line: string, end = trimEndIndex(line)): number {
99
- let index = skipWhitespace(line, 0, end);
100
- while (index < end && isDecorationCode(line.charCodeAt(index))) index++;
101
- return skipWhitespace(line, index, end);
102
- }
103
-
104
- interface NumberScan {
105
- line: number;
106
- nextIndex: number;
107
- }
108
-
109
- function scanLineNumber(line: string, index: number, end: number): NumberScan | null {
110
- if (index >= end || !isNonZeroDigitCode(line.charCodeAt(index))) return null;
111
-
112
- let lineNumber = 0;
113
- let nextIndex = index;
114
- while (nextIndex < end) {
115
- const code = line.charCodeAt(nextIndex);
116
- if (!isDigitCode(code)) break;
117
- lineNumber = lineNumber * 10 + (code - CHAR_ZERO);
118
- nextIndex++;
119
- }
120
- return { line: lineNumber, nextIndex };
121
- }
122
-
123
- /** Parse a bare line-number anchor (used by insert ops). Throws on malformed input. */
124
- export function parseLid(raw: string, lineNum: number): Anchor {
125
- const end = trimEndIndex(raw);
126
- const numberStart = skipDecoratedAnchorPrefix(raw, end);
127
- const number = scanLineNumber(raw, numberStart, end);
128
- if (number === null || skipWhitespace(raw, number.nextIndex, end) !== end) {
129
- throw new Error(
130
- `line ${lineNum}: expected a line number such as ${describeAnchorExamples("119")}; ` +
131
- `got ${JSON.stringify(raw)}. Use ${HL_FILE_PREFIX}PATH${HL_FILE_HASH_SEP}hash from your latest read for file-version binding.`,
132
- );
133
- }
134
- return { line: number.line };
135
- }
136
-
137
- export interface ParsedRange {
138
- start: Anchor;
139
- end: Anchor;
140
- }
141
-
142
- interface RangeScan {
143
- range: ParsedRange;
144
- nextIndex: number;
145
- }
146
-
147
- function scanRange(line: string, end = trimEndIndex(line)): RangeScan | null {
148
- const numberStart = skipDecoratedAnchorPrefix(line, end);
149
- const start = scanLineNumber(line, numberStart, end);
150
- if (start === null) return null;
151
-
152
- let nextIndex = start.nextIndex;
153
- let rangeEnd = start.line;
154
- if (nextIndex < end && line.charCodeAt(nextIndex) === 45) {
155
- const endNumber = scanLineNumber(line, nextIndex + 1, end);
156
- if (endNumber === null) return null;
157
- rangeEnd = endNumber.line;
158
- nextIndex = endNumber.nextIndex;
159
- }
160
-
161
- return {
162
- range: { start: { line: start.line }, end: { line: rangeEnd } },
163
- nextIndex: skipWhitespace(line, nextIndex, end),
164
- };
165
- }
166
-
167
- function startsWithWord(line: string, index: number, end: number, word: string): boolean {
168
- if (index + word.length > end) return false;
169
- for (let offset = 0; offset < word.length; offset++) {
170
- if (line.charCodeAt(index + offset) !== word.charCodeAt(offset)) return false;
171
- }
172
- return true;
173
- }
174
-
175
- function parseInsertTarget(raw: string, lineNum: number, kind: "before" | "after"): HashlineCursor {
176
- const end = trimEndIndex(raw);
177
- const targetStart = skipDecoratedAnchorPrefix(raw, end);
178
-
179
- if (startsWithWord(raw, targetStart, end, "BOF") && skipWhitespace(raw, targetStart + 3, end) === end) {
180
- return { kind: "bof" };
181
- }
182
- if (startsWithWord(raw, targetStart, end, "EOF") && skipWhitespace(raw, targetStart + 3, end) === end) {
183
- return { kind: "eof" };
184
- }
185
-
186
- const cursorKind = kind === "before" ? "before_anchor" : "after_anchor";
187
- return { kind: cursorKind, anchor: parseLid(raw, lineNum) };
188
- }
189
-
190
- function scanInlineBody(line: string, index: number): string | undefined {
191
- const end = trimEndIndex(line);
192
- return index < end ? line.slice(index, end) : undefined;
193
- }
194
-
195
- interface ParsedInsertOp {
196
- kind: "insert";
197
- cursor: HashlineCursor;
198
- inlineBody: string | undefined;
199
- }
200
-
201
- interface ParsedReplaceOp {
202
- kind: "replace";
203
- range: ParsedRange;
204
- inlineBody: string | undefined;
205
- }
206
-
207
- interface ParsedDeleteOp {
208
- kind: "delete";
209
- range: ParsedRange;
210
- trailingPayload: boolean;
211
- }
212
-
213
- type ParsedOp = ParsedInsertOp | ParsedReplaceOp | ParsedDeleteOp;
214
-
215
- function tryParseInsertOp(line: string, sigil: string, kind: "before" | "after"): ParsedInsertOp | null {
216
- const end = trimEndIndex(line);
217
- const targetStart = skipDecoratedAnchorPrefix(line, end);
218
-
219
- let targetEnd: number;
220
- if (startsWithWord(line, targetStart, end, "BOF") || startsWithWord(line, targetStart, end, "EOF")) {
221
- targetEnd = targetStart + 3;
222
- } else {
223
- const anchor = scanLineNumber(line, targetStart, end);
224
- if (anchor === null) return null;
225
- targetEnd = anchor.nextIndex;
226
- }
227
-
228
- const opIndex = skipWhitespace(line, targetEnd, end);
229
- if (opIndex >= end || line[opIndex] !== sigil) return null;
230
-
231
- // parseInsertTarget can only throw on inputs that already passed the
232
- // BOF/EOF/line-number scan above, but guard the throw anyway — the
233
- // tokenizer contract forbids it and a future refactor of the prefix
234
- // scan must not silently start raising here.
235
- try {
236
- return {
237
- kind: "insert",
238
- cursor: parseInsertTarget(line.slice(0, opIndex), 0, kind),
239
- inlineBody: scanInlineBody(line, opIndex + sigil.length),
240
- };
241
- } catch {
242
- return null;
243
- }
244
- }
245
-
246
- function tryParseReplaceOp(line: string): ParsedReplaceOp | null {
247
- const end = trimEndIndex(line);
248
- const range = scanRange(line, end);
249
- if (range === null || range.nextIndex >= end || line[range.nextIndex] !== HL_OP_REPLACE) return null;
250
- return {
251
- kind: "replace",
252
- range: range.range,
253
- inlineBody: scanInlineBody(line, range.nextIndex + HL_OP_REPLACE.length),
254
- };
255
- }
256
-
257
- function tryParseDeleteOp(line: string): ParsedDeleteOp | null {
258
- const end = trimEndIndex(line);
259
- const range = scanRange(line, end);
260
- if (range === null || range.nextIndex >= end || line[range.nextIndex] !== HL_OP_DELETE) return null;
261
- const afterSigil = range.nextIndex + HL_OP_DELETE.length;
262
- return { kind: "delete", range: range.range, trailingPayload: afterSigil !== end };
263
- }
264
-
265
- function tryParseOp(line: string): ParsedOp | null {
266
- return (
267
- tryParseInsertOp(line, HL_OP_INSERT_BEFORE, "before") ??
268
- tryParseInsertOp(line, HL_OP_INSERT_AFTER, "after") ??
269
- tryParseReplaceOp(line) ??
270
- tryParseDeleteOp(line)
271
- );
272
- }
273
-
274
- /**
275
- * Strict header scan: `¶+` prefix, optional whitespace, path body that excludes
276
- * whitespace, `#`, and `¶`, optional `#[0-9a-f]{4}` hash suffix, optional
277
- * trailing whitespace. Returns `null` when any byte deviates from the shape.
278
- */
279
- function tryParseHeader(line: string): { path: string; fileHash?: string } | null {
280
- const end = trimEndIndex(line);
281
- if (end === 0 || line.charCodeAt(0) !== CHAR_PILCROW) return null;
282
-
283
- let index = 0;
284
- while (index < end && line.charCodeAt(index) === CHAR_PILCROW) index++;
285
- index = skipWhitespace(line, index, end);
286
- if (index >= end) return null;
287
-
288
- const pathStart = index;
289
- while (index < end) {
290
- const code = line.charCodeAt(index);
291
- if (code === CHAR_HASH || code === CHAR_PILCROW || code === CHAR_SPACE || code === CHAR_TAB) break;
292
- index++;
293
- }
294
- if (index === pathStart) return null;
295
- const path = line.slice(pathStart, index);
296
-
297
- let fileHash: string | undefined;
298
- if (index < end && line.charCodeAt(index) === CHAR_HASH) {
299
- const hashStart = index + 1;
300
- const hashEnd = hashStart + FILE_HASH_LENGTH;
301
- if (hashEnd > end) return null;
302
- for (let probe = hashStart; probe < hashEnd; probe++) {
303
- if (!isHexDigitCode(line.charCodeAt(probe))) return null;
304
- }
305
- fileHash = line.slice(hashStart, hashEnd);
306
- index = hashEnd;
307
- }
308
-
309
- // Anything other than trailing whitespace disqualifies the header.
310
- if (skipWhitespace(line, index, end) !== end) return null;
311
-
312
- return fileHash !== undefined ? { path, fileHash } : { path };
313
- }
314
-
315
- /**
316
- * Returns true when the line scans as `LINE!payload` (delete sigil followed by
317
- * additional content). The executor uses this for the dedicated "deletes only"
318
- * diagnostic, separate from the standard "unrecognized op" path.
319
- */
320
- export function isDeleteOpWithPayload(line: string): boolean {
321
- const range = scanRange(line, line.length);
322
- return (
323
- range !== null &&
324
- range.nextIndex < line.length &&
325
- line[range.nextIndex] === HL_OP_DELETE &&
326
- range.nextIndex + HL_OP_DELETE.length < line.length
327
- );
328
- }
329
-
330
- interface TokenBase {
331
- /** 1-indexed line number in the original input stream. */
332
- lineNum: number;
333
- }
334
-
335
- export type HashlineToken =
336
- | (TokenBase & { kind: "blank" })
337
- | (TokenBase & { kind: "envelope-begin" })
338
- | (TokenBase & { kind: "envelope-end" })
339
- | (TokenBase & { kind: "abort" })
340
- | (TokenBase & { kind: "header"; path: string; fileHash?: string })
341
- | (TokenBase & { kind: "op-insert"; cursor: HashlineCursor; inlineBody: string | undefined })
342
- | (TokenBase & { kind: "op-replace"; range: ParsedRange; inlineBody: string | undefined })
343
- | (TokenBase & { kind: "op-delete"; range: ParsedRange; trailingPayload: boolean })
344
- | (TokenBase & { kind: "payload"; text: string })
345
- | (TokenBase & { kind: "raw"; text: string });
346
-
347
- function classifyLine(line: string, lineNum: number): HashlineToken {
348
- if (isEmptyLine(line)) return { kind: "blank", lineNum };
349
- if (markerLineEquals(line, BEGIN_PATCH_MARKER)) return { kind: "envelope-begin", lineNum };
350
- if (markerLineEquals(line, END_PATCH_MARKER)) return { kind: "envelope-end", lineNum };
351
- if (markerLineEquals(line, ABORT_MARKER)) return { kind: "abort", lineNum };
352
-
353
- if (line.charCodeAt(0) === CHAR_PILCROW) {
354
- const header = tryParseHeader(line);
355
- if (header !== null) {
356
- return header.fileHash !== undefined
357
- ? { kind: "header", lineNum, path: header.path, fileHash: header.fileHash }
358
- : { kind: "header", lineNum, path: header.path };
359
- }
360
- }
361
-
362
- if (line.charCodeAt(0) === CHAR_PAYLOAD_PREFIX) {
363
- return { kind: "payload", lineNum, text: line.slice(HL_PAYLOAD_PREFIX.length) };
364
- }
365
- const op = tryParseOp(line);
366
- if (op !== null) {
367
- if (op.kind === "insert") {
368
- return { kind: "op-insert", lineNum, cursor: op.cursor, inlineBody: op.inlineBody };
369
- }
370
- if (op.kind === "replace") {
371
- return { kind: "op-replace", lineNum, range: op.range, inlineBody: op.inlineBody };
372
- }
373
- return { kind: "op-delete", lineNum, range: op.range, trailingPayload: op.trailingPayload };
374
- }
375
-
376
- return { kind: "raw", lineNum, text: line };
377
- }
378
-
379
- /**
380
- * Stateful, line-oriented classifier for hashline diff text. Use the streaming
381
- * {@link feed}/{@link end} pair to ingest text in chunks (each completed line
382
- * emits exactly one token; a trailing partial line stays buffered until the
383
- * next chunk or {@link end}). Use the stateless {@link tokenize}/predicate
384
- * methods for callers that already hold whole lines and only need
385
- * classification without buffering.
386
- */
387
- export class HashlineTokenizer {
388
- #buffer = "";
389
- #nextLineNum = 1;
390
- #closed = false;
391
-
392
- /**
393
- * Ingest a chunk of input text. Each newline-terminated line in the
394
- * combined buffer produces one token. A trailing partial line (no `\n`
395
- * yet, possibly ending in a lone `\r`) stays buffered until the next
396
- * `feed`/`end` call so CRLF pairs that straddle chunk boundaries are
397
- * still normalized correctly.
398
- */
399
- feed(chunk: string): HashlineToken[] {
400
- if (this.#closed) throw new Error("HashlineTokenizer is closed; call reset() before reusing.");
401
- if (chunk.length === 0) return [];
402
- this.#buffer = this.#buffer ? this.#buffer + chunk : chunk;
403
- return this.#drainCompleteLines();
404
- }
405
-
406
- /**
407
- * Flush any buffered residual line (the last line of input when it lacks
408
- * a trailing newline) and mark the tokenizer closed. Calling `end` a
409
- * second time returns `[]`; reuse requires `reset`.
410
- */
411
- end(): HashlineToken[] {
412
- if (this.#closed) return [];
413
- this.#closed = true;
414
- const buf = this.#buffer;
415
- this.#buffer = "";
416
- if (buf.length === 0) return [];
417
- let stop = buf.length;
418
- if (buf.charCodeAt(stop - 1) === CHAR_CARRIAGE_RETURN) stop--;
419
- const token = classifyLine(buf.slice(0, stop), this.#nextLineNum++);
420
- return [token];
421
- }
422
-
423
- /** Discard any buffered text and reset the line counter to 1. */
424
- reset(): void {
425
- this.#buffer = "";
426
- this.#nextLineNum = 1;
427
- this.#closed = false;
428
- }
429
-
430
- /** Convenience: feed an entire text and immediately flush. */
431
- tokenizeAll(text: string): HashlineToken[] {
432
- this.reset();
433
- const first = this.feed(text);
434
- const last = this.end();
435
- return last.length === 0 ? first : first.concat(last);
436
- }
437
-
438
- /** Stateless one-shot classification. Does not touch the streaming buffer. */
439
- tokenize(line: string, lineNum = 0): HashlineToken {
440
- return classifyLine(line, lineNum);
441
- }
442
-
443
- isOp(line: string): boolean {
444
- return tryParseOp(line) !== null;
445
- }
446
-
447
- isHeader(line: string): boolean {
448
- return tryParseHeader(line) !== null;
449
- }
450
-
451
- isEnvelopeMarker(line: string): boolean {
452
- return (
453
- markerLineEquals(line, BEGIN_PATCH_MARKER) ||
454
- markerLineEquals(line, END_PATCH_MARKER) ||
455
- markerLineEquals(line, ABORT_MARKER)
456
- );
457
- }
458
-
459
- #drainCompleteLines(): HashlineToken[] {
460
- const tokens: HashlineToken[] = [];
461
- const buf = this.#buffer;
462
- let start = 0;
463
- for (let index = 0; index < buf.length; index++) {
464
- if (buf.charCodeAt(index) !== CHAR_LINE_FEED) continue;
465
- let stop = index;
466
- if (stop > start && buf.charCodeAt(stop - 1) === CHAR_CARRIAGE_RETURN) stop--;
467
- tokens.push(classifyLine(buf.slice(start, stop), this.#nextLineNum++));
468
- start = index + 1;
469
- }
470
- this.#buffer = start < buf.length ? buf.slice(start) : "";
471
- return tokens;
472
- }
473
- }
@@ -1,66 +0,0 @@
1
- import * as z from "zod/v4";
2
- import type { LspBatchRequest } from "../edit/renderer";
3
- import type { WritethroughCallback, WritethroughDeferredHandle } from "../lsp";
4
- import type { ToolSession } from "../tools";
5
-
6
- export type Anchor = {
7
- line: number;
8
- };
9
-
10
- export type HashlineCursor =
11
- | { kind: "bof" }
12
- | { kind: "eof" }
13
- | { kind: "before_anchor"; anchor: Anchor }
14
- | { kind: "after_anchor"; anchor: Anchor };
15
-
16
- export type HashlineEdit =
17
- | { kind: "insert"; cursor: HashlineCursor; text: string; lineNum: number; index: number }
18
- | { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string };
19
-
20
- export interface HashlineInputSection {
21
- path: string;
22
- fileHash?: string;
23
- diff: string;
24
- }
25
-
26
- /** `path` is accepted by the edit tool runtime; other extra keys are preserved. */
27
- export const hashlineEditParamsSchema = z.object({ input: z.string(), path: z.string().optional() }).passthrough();
28
- export type HashlineParams = z.infer<typeof hashlineEditParamsSchema>;
29
-
30
- export interface HashlineStreamOptions {
31
- /** First line number to use when formatting (1-indexed). */
32
- startLine?: number;
33
- /** Maximum formatted lines per yielded chunk (default: 200). */
34
- maxChunkLines?: number;
35
- /** Maximum UTF-8 bytes per yielded chunk (default: 64 KiB). */
36
- maxChunkBytes?: number;
37
- }
38
-
39
- export interface CompactHashlineDiffPreview {
40
- preview: string;
41
- addedLines: number;
42
- removedLines: number;
43
- }
44
-
45
- export interface CompactHashlineDiffOptions {
46
- /** Maximum entries kept on each side of an unchanged-context truncation (default: 2). */
47
- maxUnchangedRun?: number;
48
- }
49
- export interface HashlineApplyOptions {
50
- autoDropPureInsertDuplicates?: boolean;
51
- }
52
-
53
- export interface SplitHashlineOptions {
54
- cwd?: string;
55
- path?: string;
56
- }
57
-
58
- export interface ExecuteHashlineSingleOptions {
59
- session: ToolSession;
60
- input: string;
61
- path?: string;
62
- signal?: AbortSignal;
63
- batchRequest?: LspBatchRequest;
64
- writethrough: WritethroughCallback;
65
- beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
66
- }
@@ -1,83 +0,0 @@
1
- Your patch language is a compact, line-anchored edit format.
2
-
3
- <payload>
4
- Patch payload is a series of hunks: `¶PATH#HASH` header followed by any number of operations. `HASH` should be copied as is from read/search. Missing? Re-`read`.
5
- - No context rows, no gutters.
6
- - NEVER restate unchanged lines "for context".
7
- - Op lines carry NO payload. Every payload line lives on its own row and MUST start with `+`; that delimiter is stripped.
8
- - Payload indentation is literal.
9
- </payload>
10
-
11
- <ops>
12
- LINE↑ insert before (or BOF↑)
13
- LINE↓ insert after (or EOF↓)
14
- A-B: replace A..B (or A: == A..A)
15
- A-B! delete A..B (or A! == A..A)
16
- +PAYLOAD payload line for the preceding op
17
- </ops>
18
-
19
- <rules>
20
- - **Payload is only what's NEW.** `:` replaces inside; `↑`/`↓` add at anchor. NEVER repeat anchor lines or neighbors.
21
- - **Use `+` for a blank payload line; use `++text` to write a line starting with `+text`.**
22
- - **Inserts add ONLY the rows you list.** The file's existing newlines around the anchor stay. NEVER tack a trailing `+` blank "for spacing" — it writes a literal blank line into the file, doubling whatever is already there.
23
- - **A bare `LINE↑`/`LINE↓` with no payload still inserts ONE blank line.** Not a no-op. Omit the op if you want nothing there.
24
- - **Go small.** Add → `↑`/`↓`; replace → `:`; delete → `!`.
25
- - **Line numbers are frozen references to what you have seen.** Later ops in the same hunk still use original line numbers; they do NOT shift as earlier ops apply.
26
- </rules>
27
-
28
- <common-failures>
29
- - **NEVER replay past your range.** Stop before B+1; extend B if needed.
30
- - **Read lines look like replace ops.** `84:content` = "make line 84 content" — and inline content is rejected. Don't echo read-style rows.
31
- - **NEVER fabricate file hashes.** Missing? Re-`read`.
32
- </common-failures>
33
-
34
- <example>
35
- ```a.ts#1a2b
36
- 1:const X = "a";
37
- 2:
38
- 3:export function f() { return X; }
39
- 4:f();
40
- ```
41
-
42
- # replace one line, insert after, delete
43
- ```
44
- ¶a.ts#1a2b
45
- 1:
46
- +const X = "b";
47
- +export const Y = X;
48
- 1↓
49
- +const Z = Y;
50
- 4!
51
- ```
52
- </example>
53
-
54
- <anti-pattern>
55
- # WRONG — inline payload after the sigil is rejected
56
- 1:const X = "b";
57
- 1↓const Z = Y;
58
- 1-2:const X = "b";
59
- +export const Y = X;
60
- # WRONG — INSERT used to change a line (old line survives)
61
- 1↓
62
- +const X = "b";
63
- # WRONG — echoing read-style lines as context before the real op
64
- 1:const X = "a";
65
- 1-2:
66
- +const X = "b";
67
- +export const Y = X;
68
- # WRONG — trailing `+` blank writes a literal empty line; the new blank lands right next to the orig blank at line 2, doubling it
69
- 1↓
70
- +const Y = X;
71
- +
72
- # WRONG — `2↓` still anchors at PRE-EDIT line 2 (frozen), NOT at the line just inserted by `1↓`. Both inserts land at their own anchors, giving three consecutive blanks (new from `1↓`, orig blank line 2, new from `2↓`).
73
- 1↓
74
- 2↓
75
- </anti-pattern>
76
-
77
- <critical>
78
- - One op per range, ever.
79
- - Pick op precisely. Update: `:`, add: `↑`/`↓`, remove: `!`.
80
- - Payload always lives on its own `+`-prefixed line — never inline with the op.
81
- - Payload is only what's NEW; never repeat anchor lines or neighbors.
82
- - Anchor exactly; don't anchor neighbors.
83
- </critical>