@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.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.
Files changed (191) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/tree-selector.ts +10 -2
  114. package/src/modes/controllers/command-controller.ts +1 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  116. package/src/modes/controllers/selector-controller.ts +5 -5
  117. package/src/modes/types.ts +4 -1
  118. package/src/modes/utils/ui-helpers.ts +4 -0
  119. package/src/prompts/agents/explore.md +1 -1
  120. package/src/prompts/tools/ast-edit.md +1 -1
  121. package/src/prompts/tools/ast-grep.md +1 -1
  122. package/src/prompts/tools/eval.md +1 -1
  123. package/src/prompts/tools/hashline.md +73 -94
  124. package/src/prompts/tools/read.md +4 -4
  125. package/src/prompts/tools/search.md +3 -3
  126. package/src/sdk.ts +17 -23
  127. package/src/session/agent-session.ts +59 -66
  128. package/src/session/agent-storage.ts +13 -14
  129. package/src/slash-commands/acp-builtins.ts +3 -3
  130. package/src/slash-commands/types.ts +0 -6
  131. package/src/task/executor.ts +26 -57
  132. package/src/task/index.ts +8 -4
  133. package/src/tool-discovery/tool-index.ts +0 -134
  134. package/src/tools/ast-edit.ts +36 -13
  135. package/src/tools/ast-grep.ts +45 -4
  136. package/src/tools/browser/tab-worker.ts +3 -2
  137. package/src/tools/eval.ts +2 -1
  138. package/src/tools/fetch.ts +23 -14
  139. package/src/tools/index.ts +2 -8
  140. package/src/tools/irc.ts +59 -5
  141. package/src/tools/match-line-format.ts +5 -7
  142. package/src/tools/output-schema-validator.ts +132 -0
  143. package/src/tools/read.ts +142 -31
  144. package/src/tools/review.ts +23 -0
  145. package/src/tools/search-tool-bm25.ts +3 -30
  146. package/src/tools/search.ts +48 -16
  147. package/src/tools/write.ts +3 -3
  148. package/src/tools/yield.ts +32 -41
  149. package/src/utils/edit-mode.ts +1 -2
  150. package/src/utils/file-mentions.ts +2 -2
  151. package/src/web/kagi.ts +15 -6
  152. package/src/web/parallel.ts +9 -6
  153. package/src/web/scrapers/types.ts +7 -1
  154. package/src/web/scrapers/youtube.ts +13 -7
  155. package/src/web/search/index.ts +37 -11
  156. package/src/web/search/provider.ts +5 -3
  157. package/src/web/search/providers/anthropic.ts +30 -21
  158. package/src/web/search/providers/base.ts +35 -2
  159. package/src/web/search/providers/brave.ts +4 -4
  160. package/src/web/search/providers/codex.ts +118 -89
  161. package/src/web/search/providers/exa.ts +3 -2
  162. package/src/web/search/providers/gemini.ts +58 -155
  163. package/src/web/search/providers/jina.ts +4 -4
  164. package/src/web/search/providers/kagi.ts +17 -11
  165. package/src/web/search/providers/kimi.ts +29 -13
  166. package/src/web/search/providers/parallel.ts +171 -23
  167. package/src/web/search/providers/perplexity.ts +38 -37
  168. package/src/web/search/providers/searxng.ts +3 -1
  169. package/src/web/search/providers/synthetic.ts +16 -19
  170. package/src/web/search/providers/tavily.ts +23 -18
  171. package/src/web/search/providers/utils.ts +11 -17
  172. package/src/web/search/providers/zai.ts +16 -8
  173. package/dist/types/hashline/parser.d.ts +0 -7
  174. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  175. package/dist/types/tools/vim.d.ts +0 -58
  176. package/dist/types/vim/buffer.d.ts +0 -41
  177. package/dist/types/vim/commands.d.ts +0 -6
  178. package/dist/types/vim/engine.d.ts +0 -47
  179. package/dist/types/vim/parser.d.ts +0 -3
  180. package/dist/types/vim/render.d.ts +0 -25
  181. package/dist/types/vim/types.d.ts +0 -182
  182. package/src/hashline/parser.ts +0 -246
  183. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  184. package/src/prompts/tools/vim.md +0 -98
  185. package/src/tools/vim.ts +0 -949
  186. package/src/vim/buffer.ts +0 -309
  187. package/src/vim/commands.ts +0 -382
  188. package/src/vim/engine.ts +0 -2409
  189. package/src/vim/parser.ts +0 -134
  190. package/src/vim/render.ts +0 -252
  191. package/src/vim/types.ts +0 -197
@@ -0,0 +1,239 @@
1
+ import { ABORT_WARNING } from "./constants";
2
+ import { HL_OP_CHARS, HL_OP_DELETE, HL_OP_INSERT_AFTER, HL_OP_INSERT_BEFORE, HL_OP_REPLACE } from "./hash";
3
+ import {
4
+ cloneCursor,
5
+ type HashlineToken,
6
+ HashlineTokenizer,
7
+ isDeleteOpWithPayload,
8
+ type ParsedRange,
9
+ } from "./tokenizer";
10
+ import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
11
+
12
+ function validateRangeOrder(range: ParsedRange, lineNum: number): void {
13
+ if (range.end.line < range.start.line) {
14
+ throw new Error(`line ${lineNum}: range ${range.start.line}-${range.end.line} ends before it starts.`);
15
+ }
16
+ }
17
+
18
+ function expandRange(range: ParsedRange): Anchor[] {
19
+ const anchors: Anchor[] = [];
20
+ for (let line = range.start.line; line <= range.end.line; line++) {
21
+ anchors.push({ line });
22
+ }
23
+ return anchors;
24
+ }
25
+
26
+ type PendingOp =
27
+ | { kind: "insert"; cursor: HashlineCursor; lineNum: number }
28
+ | { kind: "replace"; range: ParsedRange; lineNum: number };
29
+
30
+ interface Pending {
31
+ op: PendingOp;
32
+ payload: string[];
33
+ pendingBlanks: number;
34
+ }
35
+
36
+ /**
37
+ * Token-driven state machine that turns a stream of {@link HashlineToken}s
38
+ * into the flat list of {@link HashlineEdit}s applied downstream by the
39
+ * apply/diff layers.
40
+ *
41
+ * The executor owns:
42
+ * - the running edit index (kept monotonic across pending flushes),
43
+ * - the pending-payload buffer (lines accumulated for the most recently
44
+ * opened insert/replace op),
45
+ * - all parse-time diagnostics (range order, "delete with payload",
46
+ * orphan payload, unrecognized op),
47
+ * - the {@link terminated} flag set by `envelope-end`/`abort`.
48
+ *
49
+ * Tokens are dispatched in the order they arrive; the matching tokenizer
50
+ * supplies the line numbers carried inside each token so diagnostics line
51
+ * up with the source.
52
+ */
53
+ export class HashlineExecutor {
54
+ #edits: HashlineEdit[] = [];
55
+ #warnings: string[] = [];
56
+ #editIndex = 0;
57
+ #pending: Pending | undefined;
58
+ #terminated = false;
59
+
60
+ /** True once an `envelope-end` or `abort` token has been observed. */
61
+ get terminated(): boolean {
62
+ return this.#terminated;
63
+ }
64
+
65
+ /**
66
+ * Consume one token. After `terminated` flips true subsequent feeds
67
+ * are silently ignored so callers can keep draining their tokenizer
68
+ * without explicit early-exit guards.
69
+ */
70
+ feed(token: HashlineToken): void {
71
+ if (this.#terminated) return;
72
+
73
+ switch (token.kind) {
74
+ case "envelope-begin":
75
+ return;
76
+ case "envelope-end":
77
+ this.#terminated = true;
78
+ return;
79
+ case "abort":
80
+ this.#warnings.push(ABORT_WARNING);
81
+ this.#terminated = true;
82
+ return;
83
+ case "header":
84
+ this.#flushPending(false);
85
+ return;
86
+ case "blank":
87
+ if (this.#pending) this.#pending.pendingBlanks++;
88
+ return;
89
+ case "payload":
90
+ this.#handlePayload(token.text, token.lineNum);
91
+ return;
92
+ case "op-delete":
93
+ this.#flushPending(false);
94
+ if (token.trailingPayload) {
95
+ throw new Error(
96
+ `line ${token.lineNum}: ${HL_OP_DELETE} deletes only. Payload is forbidden after ${HL_OP_DELETE}; use ${HL_OP_REPLACE} to replace.`,
97
+ );
98
+ }
99
+ validateRangeOrder(token.range, token.lineNum);
100
+ for (const anchor of expandRange(token.range)) {
101
+ this.#edits.push({ kind: "delete", anchor, lineNum: token.lineNum, index: this.#editIndex++ });
102
+ }
103
+ return;
104
+ case "op-insert":
105
+ this.#flushPending(false);
106
+ this.#pending = {
107
+ op: { kind: "insert", cursor: token.cursor, lineNum: token.lineNum },
108
+ payload: [token.inlineBody ?? ""],
109
+ pendingBlanks: 0,
110
+ };
111
+ return;
112
+ case "op-replace":
113
+ this.#flushPending(false);
114
+ validateRangeOrder(token.range, token.lineNum);
115
+ this.#pending = {
116
+ op: { kind: "replace", range: token.range, lineNum: token.lineNum },
117
+ payload: [token.inlineBody ?? ""],
118
+ pendingBlanks: 0,
119
+ };
120
+ return;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Flush any open pending op (including its trailing blank lines, which
126
+ * are payload-significant) and return the accumulated edits and
127
+ * warnings. The executor is single-use; reset() is required for reuse.
128
+ */
129
+ end(): { edits: HashlineEdit[]; warnings: string[] } {
130
+ this.#flushPending(true);
131
+ return { edits: this.#edits, warnings: this.#warnings };
132
+ }
133
+
134
+ /** Reset to a fresh state so the same instance can drive another parse. */
135
+ reset(): void {
136
+ this.#edits = [];
137
+ this.#warnings = [];
138
+ this.#editIndex = 0;
139
+ this.#pending = undefined;
140
+ this.#terminated = false;
141
+ }
142
+
143
+ #handlePayload(text: string, lineNum: number): void {
144
+ if (this.#pending) {
145
+ this.#flushPendingBlanks();
146
+ this.#pending.payload.push(text);
147
+ return;
148
+ }
149
+
150
+ // Whitespace-only payload outside any pending op is a visual
151
+ // separator (matches the legacy outer-loop isBlankLine skip);
152
+ // only fully-empty lines arrive as `blank` tokens.
153
+ if (text.trim().length === 0) return;
154
+ // Orphan payload outside any pending op: pick the most specific
155
+ // diagnostic so the model sees the actionable hint.
156
+ if (isDeleteOpWithPayload(text)) {
157
+ throw new Error(
158
+ `line ${lineNum}: ${HL_OP_DELETE} deletes only. Payload is forbidden after ${HL_OP_DELETE}; use ${HL_OP_REPLACE} to replace.`,
159
+ );
160
+ }
161
+
162
+ const firstChar = text[0];
163
+ const startsWithOp = firstChar !== undefined && HL_OP_CHARS.includes(firstChar);
164
+ if (startsWithOp || firstChar === "-" || firstChar === "@" || firstChar === "«" || firstChar === "»") {
165
+ throw new Error(
166
+ `line ${lineNum}: unrecognized op. Use LINE${HL_OP_INSERT_BEFORE} (insert before), LINE${HL_OP_INSERT_AFTER} (insert after), LINE${HL_OP_REPLACE} / A-B${HL_OP_REPLACE} (replace), or LINE${HL_OP_DELETE} / A-B${HL_OP_DELETE} (delete). ` +
167
+ `Got ${JSON.stringify(text)}.`,
168
+ );
169
+ }
170
+
171
+ throw new Error(
172
+ `line ${lineNum}: payload line has no preceding ${HL_OP_INSERT_BEFORE}, ${HL_OP_INSERT_AFTER}, ${HL_OP_REPLACE}, or ${HL_OP_DELETE} operation. ` +
173
+ `Got ${JSON.stringify(text)}.`,
174
+ );
175
+ }
176
+
177
+ #flushPendingBlanks(): void {
178
+ if (!this.#pending) return;
179
+ for (let count = 0; count < this.#pending.pendingBlanks; count++) this.#pending.payload.push("");
180
+ this.#pending.pendingBlanks = 0;
181
+ }
182
+
183
+ #flushPending(includeTrailingBlanks: boolean): void {
184
+ const pending = this.#pending;
185
+ if (!pending) return;
186
+ if (includeTrailingBlanks) this.#flushPendingBlanks();
187
+
188
+ const { op, payload } = pending;
189
+ const linesToInsert = payload;
190
+
191
+ if (op.kind === "insert") {
192
+ for (const text of linesToInsert) {
193
+ this.#edits.push({
194
+ kind: "insert",
195
+ cursor: cloneCursor(op.cursor),
196
+ text,
197
+ lineNum: op.lineNum,
198
+ index: this.#editIndex++,
199
+ });
200
+ }
201
+ } else {
202
+ for (const text of linesToInsert) {
203
+ this.#edits.push({
204
+ kind: "insert",
205
+ cursor: { kind: "before_anchor", anchor: { ...op.range.start } },
206
+ text,
207
+ lineNum: op.lineNum,
208
+ index: this.#editIndex++,
209
+ });
210
+ }
211
+ for (const anchor of expandRange(op.range)) {
212
+ this.#edits.push({ kind: "delete", anchor, lineNum: op.lineNum, index: this.#editIndex++ });
213
+ }
214
+ }
215
+
216
+ this.#pending = undefined;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Drive a full hashline diff through the tokenizer + executor pipeline and
222
+ * return the resulting edits plus any parse-time warnings. This is the
223
+ * convenience entry point most callers want; reach for {@link
224
+ * HashlineTokenizer}/{@link HashlineExecutor} directly only when you need
225
+ * streaming feeds, cross-section state, or custom token handling.
226
+ */
227
+ export function parseHashline(diff: string): { edits: HashlineEdit[]; warnings: string[] } {
228
+ const tokenizer = new HashlineTokenizer();
229
+ const executor = new HashlineExecutor();
230
+ const drain = (tokens: HashlineToken[]): void => {
231
+ for (const token of tokens) {
232
+ if (executor.terminated) return;
233
+ executor.feed(token);
234
+ }
235
+ };
236
+ drain(tokenizer.feed(diff));
237
+ drain(tokenizer.end());
238
+ return executor.end();
239
+ }
@@ -3,19 +3,21 @@ begin_patch: "*** Begin Patch" LF
3
3
  end_patch: "*** End Patch" LF?
4
4
 
5
5
  hunk: update_hunk
6
- update_hunk: "$HFILE$" filename LF line_op*
6
+ update_hunk: "$HFILE$" filename ("#" file_hash)? LF line_op*
7
7
 
8
- filename: /(.+)/
8
+ filename: /([^\s#]+)/
9
+ file_hash: /[0-9a-f]{4}/
9
10
 
10
- line_op: insert_before | insert_after | replace | blank
11
- insert_before: "$HOP_INSERT_BEFORE$" anchor LF payload+
12
- insert_after: "$HOP_INSERT_AFTER$" anchor LF payload+
13
- replace: "$HOP_REPLACE$" range LF payload*
14
- payload: /[^$HOP_CHARS$$HFILE$\n][^\n]*/ LF | LF
15
- blank: LF
11
+ line_op: insert_before | insert_after | replace | delete
12
+ insert_before: anchor "$HOP_INSERT_BEFORE$" inline_body? LF payload*
13
+ insert_after: anchor "$HOP_INSERT_AFTER$" inline_body? LF payload*
14
+ replace: range "$HOP_REPLACE$" inline_body? LF payload*
15
+ delete: range "$HOP_DELETE$" LF
16
+ inline_body: /[^\n]+/
17
+ payload: /(.*)/ LF
16
18
 
17
19
  anchor: LID | "EOF" | "BOF"
18
- range: LID (".." LID)?
19
- LID: /[1-9]\d*$HFMT$/
20
+ range: LID ("-" LID)?
21
+ LID: /[1-9]\d*/
20
22
 
21
23
  %import common.LF
@@ -3,70 +3,54 @@
3
3
  * and prompt helpers.
4
4
  */
5
5
 
6
- import bigrams from "./bigrams.json" with { type: "json" };
7
-
8
- /**
9
- * 647 single-token BPE bigrams for hashline anchors. Every entry tokenizes as
10
- * exactly one token in modern BPE vocabularies (cl100k / o200k / Claude family),
11
- * so a hashline anchor built from one bigram is exactly 1 token.
12
- *
13
- * This is the complete set of 2-letter lowercase combinations that are single
14
- * tokens — the 29 missing combinations are rare-letter pairs (q/x/z heavy)
15
- * that no major BPE vocabulary merges into a single token.
16
- *
17
- * Order is stable forever — changing it would invalidate every saved
18
- * `LINE+ID` reference in transcripts and prompts.
19
- */
20
- export const HL_BIGRAMS: readonly string[] = bigrams;
21
-
22
- export const HL_BIGRAMS_COUNT = HL_BIGRAMS.length;
6
+ const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23
7
 
24
8
  /**
25
- * Decoration prefix that may precede a `LINE+HASH` anchor in tool output:
9
+ * Decoration prefix that may precede a line number in tool output:
26
10
  * `>` (context line in grep), `+` (added line in diff), `-` (removed line),
27
11
  * `*` (match line). Any combination, in any order, surrounded by optional
28
- * whitespace. Output formatters emit at most one decoration per anchor; the
29
- * regex stays liberal because anchor-ref parsers accept whatever the model
30
- * echoes back.
12
+ * whitespace. Output formatters emit at most one decoration per line; the
13
+ * parser stays liberal because it accepts whatever the model echoes back.
31
14
  */
32
15
  export const HL_ANCHOR_DECORATION_RE_RAW = `\\s*[>+\\-*]*\\s*`;
33
16
 
34
- /**
35
- * Capture-group regex source for a decorated `LINE+HASH` anchor. Group 1
36
- * captures the line number (digits only); group 2 captures the hash. The
37
- * source is intentionally unanchored — anchoring with `^` (or composing into a
38
- * larger pattern) is the caller's responsibility.
39
- */
40
- export const HL_ANCHOR_RE_RAW = `${HL_ANCHOR_DECORATION_RE_RAW}(\\d+)([a-z]{2})`;
17
+ /** Capture-group regex source for a decorated bare line-number anchor. */
18
+ export const HL_ANCHOR_RE_RAW = `${HL_ANCHOR_DECORATION_RE_RAW}(\\d+)`;
41
19
 
42
- /**
43
- * Bare `LINE+HASH` Lid (no decorations, no captures, no anchors). Use for
44
- * embedding inside larger patterns where the line+hash unit appears as a
45
- * literal (e.g. range bounds, alternation arms, op-line heuristics).
46
- */
47
- export const HL_HASH_RE_RAW = `[1-9]\\d*[a-z]{2}`;
20
+ /** Bare positive line-number Lid (no decorations, no captures, no anchors). */
21
+ export const HL_LINE_RE_RAW = `[1-9]\\d*`;
48
22
 
49
- /**
50
- * Capture-group form of {@link HL_HASH_RE_RAW}: group 1 captures the
51
- * line number, group 2 captures the hash.
52
- */
53
- export const HL_HASH_CAPTURE_RE_RAW = `([1-9]\\d*)([a-z]{2})`;
23
+ /** Capture-group form of {@link HL_LINE_RE_RAW}. */
24
+ export const HL_LINE_CAPTURE_RE_RAW = `([1-9]\\d*)`;
25
+
26
+ /** Four-hex-character file hash carried by a hashline section header. */
27
+ export const HL_FILE_HASH_RE_RAW = `[0-9a-f]{4}`;
28
+
29
+ /** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
30
+ export const HL_FILE_HASH_CAPTURE_RE_RAW = `(${HL_FILE_HASH_RE_RAW})`;
31
+
32
+ /** Separator between a hashline file path and its file hash. */
33
+ export const HL_FILE_HASH_SEP = "#";
34
+
35
+ /** Separator between a line number and displayed line content in hashline mode. */
36
+ export const HL_LINE_BODY_SEP = ":";
54
37
 
55
- /** Width of a hash in display characters. */
56
- export const HL_HASH_WIDTH = 2;
38
+ /** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
39
+ export const HL_LINE_BODY_SEP_RE_RAW = regexEscape(HL_LINE_BODY_SEP);
57
40
 
58
41
  /**
59
- * Representative hash suffixes for use in user-facing error messages and
60
- * prompt examples.
42
+ * Representative file hashes for use in user-facing error messages and prompt
43
+ * examples.
61
44
  */
62
- export const HL_HASH_EXAMPLES = ["sr", "ab", "th"] as const;
45
+ export const HL_FILE_HASH_EXAMPLES = ["1a2b", "3c4d", "9f3e"] as const;
63
46
 
64
47
  /**
65
48
  * Format a comma-separated list of example anchors with an optional line-number
66
- * prefix, quoted for inclusion in error messages: `"160sr", "160ab", "160th"`.
49
+ * prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
67
50
  */
68
51
  export function describeAnchorExamples(linePrefix = ""): string {
69
- return HL_HASH_EXAMPLES.map(e => `"${linePrefix}${e}"`).join(", ");
52
+ const examples = linePrefix ? [linePrefix, `${linePrefix.slice(0, -1) || "4"}2`, "7"] : ["160", "42", "7"];
53
+ return examples.map(e => `"${e}"`).join(", ");
70
54
  }
71
55
 
72
56
  /**
@@ -76,98 +60,69 @@ export function describeAnchorExamples(linePrefix = ""): string {
76
60
  */
77
61
  export function resolveHashlineGrammarPlaceholders(grammar: string): string {
78
62
  return grammar
79
- .replaceAll("$HFMT$", "[a-z]{2}")
63
+ .replaceAll("$HFMT$", "")
64
+ .replaceAll("$HFILE_HASH$", HL_FILE_HASH_RE_RAW)
65
+ .replaceAll("$HFILE_HASH_SEP$", HL_FILE_HASH_SEP)
80
66
  .replaceAll("$HOP_INSERT_BEFORE$", HL_OP_INSERT_BEFORE)
81
67
  .replaceAll("$HOP_INSERT_AFTER$", HL_OP_INSERT_AFTER)
82
68
  .replaceAll("$HOP_REPLACE$", HL_OP_REPLACE)
69
+ .replaceAll("$HOP_DELETE$", HL_OP_DELETE)
83
70
  .replaceAll("$HOP_CHARS$", HL_OP_CHARS)
84
71
  .replaceAll("$HFILE$", HL_FILE_PREFIX);
85
72
  }
86
73
 
87
- /** @deprecated Use {@link resolveHashlineGrammarPlaceholders}. */
88
- export const resolveLarkLidPlaceholders = resolveHashlineGrammarPlaceholders;
89
-
90
- const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
-
92
74
  /**
93
- * Hashline edit input markers. File section headers start with {@link HL_FILE_PREFIX};
94
- * op lines start with a direction/action sigil: {@link HL_OP_INSERT_BEFORE},
95
- * {@link HL_OP_INSERT_AFTER}, or {@link HL_OP_REPLACE}. Payload lines are
96
- * verbatim file content and have no per-line marker.
75
+ * op lines have an `ANCHOR<SIGIL>[INLINE_PAYLOAD]` shape, where SIGIL is one of
76
+ * {@link HL_OP_INSERT_BEFORE}, {@link HL_OP_INSERT_AFTER}, {@link HL_OP_REPLACE},
77
+ * or {@link HL_OP_DELETE}.
78
+ * Multi-line payloads follow on subsequent lines as verbatim file content with no
79
+ * per-line marker.
97
80
  *
98
81
  * These constants are the single source of truth for the edit parser, grammar,
99
82
  * renderer, and prompt.
100
83
  */
101
- export const HL_OP_INSERT_BEFORE = "«";
102
- export const HL_OP_INSERT_AFTER = "»";
103
- export const HL_OP_REPLACE = "";
84
+ export const HL_OP_INSERT_BEFORE = "";
85
+ export const HL_OP_INSERT_AFTER = "";
86
+ export const HL_OP_REPLACE = ":";
87
+ export const HL_OP_DELETE = "!";
104
88
 
105
89
  /** All hashline edit op sigils, concatenated for fast membership tests. */
106
- export const HL_OP_CHARS = `${HL_OP_INSERT_BEFORE}${HL_OP_INSERT_AFTER}${HL_OP_REPLACE}`;
90
+ export const HL_OP_CHARS = `${HL_OP_INSERT_BEFORE}${HL_OP_INSERT_AFTER}${HL_OP_REPLACE}${HL_OP_DELETE}`;
107
91
 
108
92
  /** Hashline edit file section header marker. */
109
- export const HL_FILE_PREFIX = "§";
110
-
111
- /** Stable separator for read/search/hashline display output. Intentionally not configurable. */
112
- export const HL_BODY_SEP = "|";
113
-
114
- /** Regex-escaped form of {@link HL_BODY_SEP}, safe for embedding inside a regex. */
115
- export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
93
+ export const HL_FILE_PREFIX = "";
94
+
95
+ function normalizeFileHashText(text: string): string {
96
+ return text
97
+ .replace(/\r/g, "")
98
+ .split("\n")
99
+ .map(line => line.trimEnd())
100
+ .join("\n");
101
+ }
116
102
 
117
103
  /**
118
- * Compute a 2-character hash of a single line via xxHash32 mod 647 over
119
- * {@link HL_BIGRAMS}. The hash depends only on the line's content (after
120
- * stripping CR and trailing whitespace); the `idx` parameter is accepted
121
- * for call-site symmetry with line numbers but is intentionally unused so
122
- * that anchors remain stable across line shifts caused by sibling edits.
123
- *
124
- * The line input should not include a trailing newline.
104
+ * Compute the 4-hex-character hash carried by a hashline section header.
105
+ * The hash normalizes CR characters and trailing whitespace before hashing so
106
+ * platform line endings and display-trimmed lines do not invalidate anchors.
125
107
  */
126
- export function computeLineHash(idx: number, line: string): string {
127
- void idx;
128
- line = line.replace(/\r/g, "").trimEnd();
129
- // Seed is fixed so the hash depends only on line content. Earlier we mixed
130
- // in `idx` for blank/punctuation-only lines, but that meant any line shift
131
- // (e.g. from a sibling edit in the same batch) invalidated anchors whose
132
- // content had not changed. Identical blank lines are intentionally allowed
133
- // to collide — the edit op's line number disambiguates them.
134
- return HL_BIGRAMS[Bun.hash.xxHash32(line, 0) % HL_BIGRAMS_COUNT];
108
+ export function computeFileHash(text: string): string {
109
+ const normalized = normalizeFileHashText(text);
110
+ const low16 = Bun.hash.xxHash32(normalized, 0) & 0xffff;
111
+ return low16.toString(16).padStart(4, "0");
135
112
  }
136
113
 
137
- /**
138
- * Formats an anchor reference given a line number and its text.
139
- * Returns `LINE+ID` (e.g., `42sr`) — no separator between
140
- * number and hash.
141
- */
142
- export function formatLineHash(line: number, lines: string): string {
143
- return `${line}${computeLineHash(line, lines)}`;
114
+ /** Format a hashline section header for a file path and file hash. */
115
+ export function formatHashlineHeader(filePath: string, fileHash: string): string {
116
+ return `${HL_FILE_PREFIX}${filePath}${HL_FILE_HASH_SEP}${fileHash}`;
144
117
  }
145
118
 
146
- /**
147
- * Formats a single line with a hashline anchor.
148
- * Returns `LINE+ID|TEXT` (e.g., `42sr|function hi() {`, `3ab|}`).
149
- */
150
- export function formatHashLine(lineNumber: number, line: string): string {
151
- return `${lineNumber}${computeLineHash(lineNumber, line)}${HL_BODY_SEP}${line}`;
119
+ /** Formats a single numbered line as `LINE:TEXT`. */
120
+ export function formatNumberedLine(lineNumber: number, line: string): string {
121
+ return `${lineNumber}${HL_LINE_BODY_SEP}${line}`;
152
122
  }
153
123
 
154
- /**
155
- * Format file text with hashline prefixes for display.
156
- *
157
- * Each line becomes `LINE+ID|TEXT` where LINENUM is 1-indexed.
158
- * No padding on line numbers; pipe separator between anchor and content.
159
- *
160
- * @param text - Raw file text string
161
- * @param startLine - First line number (1-indexed, defaults to 1)
162
- * @returns Formatted string with one hashline-prefixed line per input line
163
- *
164
- * @example
165
- * ```
166
- * formatHashLines("function hi() {\n return;\n}")
167
- * // "1bm|function hi() {\n2er| return;\n3ab|}"
168
- * ```
169
- */
170
- export function formatHashLines(text: string, startLine = 1): string {
124
+ /** Format file text with hashline-mode line-number prefixes for display. */
125
+ export function formatNumberedLines(text: string, startLine = 1): string {
171
126
  const lines = text.split("\n");
172
- return lines.map((line, i) => formatHashLine(startLine + i, line)).join("\n");
127
+ return lines.map((line, i) => formatNumberedLine(startLine + i, line)).join("\n");
173
128
  }
@@ -4,10 +4,11 @@ export * from "./constants";
4
4
  export * from "./diff";
5
5
  export * from "./diff-preview";
6
6
  export * from "./execute";
7
+ export * from "./executor";
7
8
  export * from "./hash";
8
9
  export * from "./input";
9
- export * from "./parser";
10
10
  export * from "./prefixes";
11
11
  export * from "./recovery";
12
12
  export * from "./stream";
13
+ export * from "./tokenizer";
13
14
  export * from "./types";