pi-hashline-edit-pro 4.2.5 → 4.2.7
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.md +7 -4
- package/index.ts +18 -9
- package/package.json +1 -1
- package/src/anchor-registry.ts +262 -36
- package/src/constants.ts +3 -0
- package/src/grep.ts +154 -152
- package/src/hash-store.ts +75 -4
- package/src/hashline/resolve.ts +3 -2
- package/src/insert.ts +78 -81
- package/src/payload-contract.ts +3 -1
- package/src/read.ts +53 -51
- package/src/replace-diff.ts +8 -1
- package/src/replace-undo.ts +109 -107
- package/src/replace.ts +73 -77
- package/src/utils.ts +100 -60
- package/src/write-hook.ts +3 -3
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { NUL_CONTENT_MSG } from "./constants";
|
|
2
|
+
|
|
1
3
|
export function isRec(value: unknown): value is Record<string, unknown> {
|
|
2
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
5
|
}
|
|
@@ -74,6 +76,10 @@ export function abortIf(signal?: AbortSignal): void {
|
|
|
74
76
|
if (signal?.aborted) throw new Error("Operation aborted");
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
export function assertNoNul(lines: string[]): void {
|
|
80
|
+
if (lines.some((line) => line.includes("\0"))) throw new Error(NUL_CONTENT_MSG);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
export function errCode(error: unknown): string | undefined {
|
|
78
84
|
if (error instanceof Error) {
|
|
79
85
|
return (error as NodeJS.ErrnoException).code;
|
|
@@ -181,83 +187,117 @@ function formatLineLimit(displayPath: string, limit: number, count: number | und
|
|
|
181
187
|
return `[E_FILE_TOO_LARGE] ${displayPath} ${detail} lines, exceeding the ${limit}-line hashline limit. For very large files, use write.`;
|
|
182
188
|
}
|
|
183
189
|
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (value === "\f") return "\\f";
|
|
189
|
-
if (value === "\r") return "\\r";
|
|
190
|
-
const hex = value.charCodeAt(0).toString(16).padStart(4, "0");
|
|
191
|
-
return `\\u${hex}`;
|
|
190
|
+
function stripCodeFence(text: string): string {
|
|
191
|
+
const trimmed = text.trim();
|
|
192
|
+
const fenced = /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(trimmed);
|
|
193
|
+
return fenced ? fenced[1]!.trim() : trimmed;
|
|
192
194
|
}
|
|
193
195
|
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
function jsonStringArray(text: string): string[] | undefined {
|
|
197
|
+
try {
|
|
198
|
+
const parsed: unknown = JSON.parse(text);
|
|
199
|
+
if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === "string")) {
|
|
200
|
+
return parsed as string[];
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
198
203
|
}
|
|
199
|
-
return
|
|
204
|
+
return undefined;
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
function
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
207
|
+
function decodeEscape(char: string): string | undefined {
|
|
208
|
+
switch (char) {
|
|
209
|
+
case "\"": return "\"";
|
|
210
|
+
case "'": return "'";
|
|
211
|
+
case "\\": return "\\";
|
|
212
|
+
case "/": return "/";
|
|
213
|
+
case "b": return "\b";
|
|
214
|
+
case "f": return "\f";
|
|
215
|
+
case "n": return "\n";
|
|
216
|
+
case "r": return "\r";
|
|
217
|
+
case "t": return "\t";
|
|
218
|
+
default: return undefined;
|
|
211
219
|
}
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
function
|
|
215
|
-
const
|
|
216
|
-
let
|
|
217
|
-
let
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
|
|
222
|
-
if (escaped)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
current = "";
|
|
222
|
+
function scanQuotedSegment(inner: string, start: number): { value: string; next: number } | undefined {
|
|
223
|
+
const quote = inner[start]!;
|
|
224
|
+
let out = "";
|
|
225
|
+
let index = start + 1;
|
|
226
|
+
while (index < inner.length) {
|
|
227
|
+
const char = inner[index]!;
|
|
228
|
+
if (char === "\\") {
|
|
229
|
+
const escaped = inner[index + 1];
|
|
230
|
+
if (escaped === undefined) return undefined;
|
|
231
|
+
if (escaped === "u") {
|
|
232
|
+
const hex = inner.slice(index + 2, index + 6);
|
|
233
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) return undefined;
|
|
234
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
235
|
+
index += 6;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const decoded = decodeEscape(escaped);
|
|
239
|
+
if (decoded === undefined) return undefined;
|
|
240
|
+
out += decoded;
|
|
241
|
+
index += 2;
|
|
235
242
|
continue;
|
|
236
243
|
}
|
|
237
|
-
|
|
244
|
+
if (char === quote) return { value: out, next: index + 1 };
|
|
245
|
+
out += char;
|
|
246
|
+
index += 1;
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function scanArrayText(inner: string): string[] | undefined {
|
|
252
|
+
const values: string[] = [];
|
|
253
|
+
let index = 0;
|
|
254
|
+
while (index < inner.length && /\s/.test(inner[index]!)) index += 1;
|
|
255
|
+
if (index >= inner.length) return [];
|
|
256
|
+
for (;;) {
|
|
257
|
+
if (inner[index] !== "\"" && inner[index] !== "'") return undefined;
|
|
258
|
+
const segment = scanQuotedSegment(inner, index);
|
|
259
|
+
if (!segment) return undefined;
|
|
260
|
+
values.push(segment.value);
|
|
261
|
+
index = segment.next;
|
|
262
|
+
while (index < inner.length && /\s/.test(inner[index]!)) index += 1;
|
|
263
|
+
if (index >= inner.length) return values;
|
|
264
|
+
if (inner[index] !== ",") return undefined;
|
|
265
|
+
index += 1;
|
|
266
|
+
while (index < inner.length && /\s/.test(inner[index]!)) index += 1;
|
|
267
|
+
if (index >= inner.length) return values;
|
|
238
268
|
}
|
|
239
|
-
if (inQuotes || escaped) return undefined;
|
|
240
|
-
segments.push(current);
|
|
241
|
-
return segments;
|
|
242
269
|
}
|
|
243
270
|
|
|
244
271
|
function decodeArrayText(value: unknown): string[] | undefined {
|
|
245
272
|
if (typeof value !== "string") return undefined;
|
|
246
|
-
const trimmed = value
|
|
273
|
+
const trimmed = stripCodeFence(value);
|
|
247
274
|
if (!trimmed.startsWith("[") || !trimmed.endsWith("]")) return undefined;
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
275
|
+
const decoded = jsonStringArray(trimmed);
|
|
276
|
+
if (decoded !== undefined && decoded.length > 0) return decoded;
|
|
277
|
+
const scanned = scanArrayText(trimmed.slice(1, -1));
|
|
278
|
+
return scanned !== undefined && scanned.length > 0 ? scanned : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function looksLikeStringArray(value: unknown): boolean {
|
|
282
|
+
if (typeof value !== "string") return false;
|
|
283
|
+
const trimmed = stripCodeFence(value);
|
|
284
|
+
return trimmed.endsWith("]") && /^\[\s*['"]/.test(trimmed);
|
|
257
285
|
}
|
|
258
286
|
|
|
259
|
-
export function decodeStringArray(value: unknown): string[] | undefined {
|
|
260
|
-
|
|
261
|
-
|
|
287
|
+
export function decodeStringArray(value: unknown, warnings?: string[], label = "replacement_lines"): string[] | undefined {
|
|
288
|
+
const candidate = typeof value === "string"
|
|
289
|
+
? value
|
|
290
|
+
: Array.isArray(value) && value.length === 1 && typeof value[0] === "string"
|
|
291
|
+
? value[0]
|
|
292
|
+
: undefined;
|
|
293
|
+
if (candidate === undefined) return undefined;
|
|
294
|
+
const decoded = decodeArrayText(candidate);
|
|
295
|
+
if (decoded !== undefined) {
|
|
296
|
+
warnings?.push(`[W_BAD_SHAPE] Unwrapped JSON array syntax from a ${label} element.`);
|
|
297
|
+
return decoded;
|
|
298
|
+
}
|
|
299
|
+
if (looksLikeStringArray(candidate)) {
|
|
300
|
+
warnings?.push(`[W_BAD_SHAPE] ${label} looked like a JSON array but could not be parsed; kept as one literal line: ${clipLine(candidate, 60)}`);
|
|
301
|
+
}
|
|
262
302
|
return undefined;
|
|
263
303
|
}
|
package/src/write-hook.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
3
3
|
import { HASH_SEP } from "./hashline/hash";
|
|
4
|
-
import { servedForPath } from "./anchor-registry";
|
|
4
|
+
import { servedForPath, withAnchorSession } from "./anchor-registry";
|
|
5
5
|
import { resolveInCwd } from "./fs-write";
|
|
6
6
|
import { abortIf, splitLines, isRec, normalizeFilePath } from "./utils";
|
|
7
7
|
|
|
@@ -35,7 +35,7 @@ export async function servedHashEchoDenial(rawPath: string, content: string, cwd
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export function registerWriteHook(pi: ExtensionAPI): void {
|
|
38
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
38
|
+
pi.on("tool_call", async (event, ctx) => withAnchorSession(ctx, async () => {
|
|
39
39
|
if (event.toolName !== "write") return;
|
|
40
40
|
const input = event.input as Record<string, unknown> | undefined;
|
|
41
41
|
if (!input || !isRec(input)) return;
|
|
@@ -53,5 +53,5 @@ export function registerWriteHook(pi: ExtensionAPI): void {
|
|
|
53
53
|
console.error("write hook failed:", error);
|
|
54
54
|
}
|
|
55
55
|
return;
|
|
56
|
-
});
|
|
56
|
+
}));
|
|
57
57
|
}
|