pi-hashline-edit-pro 2.5.2 → 2.5.3
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/index.ts +2 -7
- package/package.json +1 -1
- package/src/config.ts +7 -5
- package/src/file-reader.ts +12 -0
- package/src/hash-store.ts +30 -25
- package/src/hashline/index.ts +0 -5
- package/src/hashline/resolve.ts +0 -8
- package/src/read.ts +6 -23
- package/src/replace-response.ts +1 -1
- package/src/replace-undo.ts +2 -7
- package/src/replace.ts +9 -38
- package/src/served.ts +24 -2
- package/src/utils.ts +9 -0
package/index.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
toggleAutoRead,
|
|
14
14
|
} from "./src/config";
|
|
15
15
|
import { loadHashStore, pruneMissing } from "./src/hash-store";
|
|
16
|
-
import {
|
|
16
|
+
import { recordServedSafe, clearServed } from "./src/served";
|
|
17
17
|
import { readNormFile } from "./src/file-reader";
|
|
18
18
|
import { loadFileKindAndText } from "./src/file-kind";
|
|
19
19
|
import { toCwd } from "./src/paths";
|
|
@@ -88,12 +88,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
88
88
|
DEFAULT_MAX_BYTES,
|
|
89
89
|
AUTO_READ_MAX,
|
|
90
90
|
);
|
|
91
|
-
|
|
92
|
-
const store = await loadHashStore();
|
|
93
|
-
recordServed(store, absolutePath, preview.servedHashes);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error("Failed to record served state from auto-read:", error);
|
|
96
|
-
}
|
|
91
|
+
await recordServedSafe(absolutePath, preview.servedHashes, "auto-read");
|
|
97
92
|
return {
|
|
98
93
|
content: [
|
|
99
94
|
...(event.content ?? []),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/undo tools for pi-coding-agent. Every line gets a unique 3-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import { configPath } from "./paths";
|
|
3
|
-
import { errCode } from "./utils";
|
|
3
|
+
import { errCode, isRec } from "./utils";
|
|
4
4
|
import { writeAtomic } from "./fs-write";
|
|
5
5
|
|
|
6
6
|
export interface Config {
|
|
@@ -12,10 +12,12 @@ const DEFAULT_CONFIG: Config = {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
function parseConfig(content: string): Config {
|
|
15
|
-
const parsed = JSON.parse(content) as
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const parsed = JSON.parse(content) as unknown;
|
|
16
|
+
const autoRead = isRec(parsed) ? parsed.autoRead : undefined;
|
|
17
|
+
if (typeof autoRead !== "boolean") {
|
|
18
|
+
throw new Error("config.json must be an object with a boolean autoRead field");
|
|
19
|
+
}
|
|
20
|
+
return { autoRead };
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
|
package/src/file-reader.ts
CHANGED
|
@@ -45,6 +45,18 @@ export async function fileSnap(absolutePath: string): Promise<SnapInfo> {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export async function safeSnapId(
|
|
49
|
+
absolutePath: string,
|
|
50
|
+
context: string,
|
|
51
|
+
): Promise<string | undefined> {
|
|
52
|
+
try {
|
|
53
|
+
return (await fileSnap(absolutePath)).snapshotId;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`Failed to compute snapshot (${context}):`, error);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
export interface ReadNormOptions {
|
|
49
61
|
signal?: AbortSignal;
|
|
50
62
|
accessMode?: number;
|
package/src/hash-store.ts
CHANGED
|
@@ -286,7 +286,11 @@ async function openStore(storePath: string): Promise<HashStore> {
|
|
|
286
286
|
const { db, stmts } = opened;
|
|
287
287
|
|
|
288
288
|
if (!existed) {
|
|
289
|
-
|
|
289
|
+
try {
|
|
290
|
+
await migrateLegacy(db);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error("Hash store migration failed; continuing without legacy import:", error);
|
|
293
|
+
}
|
|
290
294
|
}
|
|
291
295
|
cachedDb = { path: storePath, db, stmts };
|
|
292
296
|
|
|
@@ -328,20 +332,19 @@ export function shutdownHashStore(): void {
|
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
function withStore(fn: () => void): void {
|
|
331
|
-
if (cachedDb) {
|
|
332
|
-
|
|
333
|
-
cachedDb!.db.exec("BEGIN IMMEDIATE");
|
|
334
|
-
try {
|
|
335
|
-
fn();
|
|
336
|
-
cachedDb!.db.exec("COMMIT");
|
|
337
|
-
} catch (e) {
|
|
338
|
-
try { cachedDb!.db.exec("ROLLBACK"); } catch {}
|
|
339
|
-
throw e;
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
} else {
|
|
343
|
-
fn();
|
|
335
|
+
if (!cachedDb) {
|
|
336
|
+
throw new Error("Hash store is not open; transactional update aborted");
|
|
344
337
|
}
|
|
338
|
+
withBusyRetry(() => {
|
|
339
|
+
cachedDb!.db.exec("BEGIN IMMEDIATE");
|
|
340
|
+
try {
|
|
341
|
+
fn();
|
|
342
|
+
cachedDb!.db.exec("COMMIT");
|
|
343
|
+
} catch (e) {
|
|
344
|
+
try { cachedDb!.db.exec("ROLLBACK"); } catch {}
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
345
348
|
}
|
|
346
349
|
|
|
347
350
|
async function migrateLegacy(db: DatabaseSync): Promise<void> {
|
|
@@ -388,17 +391,19 @@ async function migrateLegacy(db: DatabaseSync): Promise<void> {
|
|
|
388
391
|
]);
|
|
389
392
|
}
|
|
390
393
|
if (rows.length > 0) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
394
|
+
withBusyRetry(() => {
|
|
395
|
+
db.exec("BEGIN IMMEDIATE");
|
|
396
|
+
try {
|
|
397
|
+
const stmt = db.prepare(
|
|
398
|
+
"INSERT OR REPLACE INTO snapshots (path, checksum, line_count, hashes, updated_at) VALUES (?, ?, ?, ?, ?)"
|
|
399
|
+
);
|
|
400
|
+
for (const row of rows) stmt.run(...row);
|
|
401
|
+
db.exec("COMMIT");
|
|
402
|
+
} catch (e) {
|
|
403
|
+
try { db.exec("ROLLBACK"); } catch {}
|
|
404
|
+
throw e;
|
|
405
|
+
}
|
|
406
|
+
});
|
|
402
407
|
}
|
|
403
408
|
|
|
404
409
|
try {
|
package/src/hashline/index.ts
CHANGED
|
@@ -6,9 +6,6 @@ export {
|
|
|
6
6
|
HASH_SPACE,
|
|
7
7
|
HASH_PROBE_STRIDE,
|
|
8
8
|
MAX_HASH_LINES,
|
|
9
|
-
HL_PREFIX_PLUS_RE,
|
|
10
|
-
HL_PREFIX_MINUS_RE,
|
|
11
|
-
HL_BARE_PREFIX_RE,
|
|
12
9
|
lineHashes,
|
|
13
10
|
_lineHashesPure,
|
|
14
11
|
initHasher,
|
|
@@ -22,7 +19,6 @@ export {
|
|
|
22
19
|
} from "./parse";
|
|
23
20
|
|
|
24
21
|
export {
|
|
25
|
-
type RAnchor,
|
|
26
22
|
type HEdit,
|
|
27
23
|
type RHEdit,
|
|
28
24
|
type HTEdit,
|
|
@@ -34,7 +30,6 @@ export {
|
|
|
34
30
|
stripBarePrefixes,
|
|
35
31
|
stripDiffPrefixes,
|
|
36
32
|
swapReversedRanges,
|
|
37
|
-
fmtMismatch,
|
|
38
33
|
findNewEdge,
|
|
39
34
|
assertRangeServed,
|
|
40
35
|
RangeStaleError,
|
package/src/hashline/resolve.ts
CHANGED
|
@@ -133,14 +133,6 @@ export function fmtMismatchWithHashes(
|
|
|
133
133
|
return { text: out.join("\n"), hashes };
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function fmtMismatch(
|
|
137
|
-
mismatches: HMismatch[],
|
|
138
|
-
fileLines: string[],
|
|
139
|
-
fileHashes: string[],
|
|
140
|
-
filePath?: string,
|
|
141
|
-
): string {
|
|
142
|
-
return fmtMismatchWithHashes(mismatches, fileLines, fileHashes, filePath).text;
|
|
143
|
-
}
|
|
144
136
|
|
|
145
137
|
const ITEM_KS = new Set(["replacement_text", "remove_from", "remove_to"]);
|
|
146
138
|
|
package/src/read.ts
CHANGED
|
@@ -9,13 +9,11 @@ import {
|
|
|
9
9
|
import { Type } from "typebox";
|
|
10
10
|
import { MAX_READ_LINE_BYTES } from "./constants";
|
|
11
11
|
import { loadFileKindAndText } from "./file-kind";
|
|
12
|
-
import { readNormFile } from "./file-reader";
|
|
12
|
+
import { readNormFile, safeSnapId } from "./file-reader";
|
|
13
13
|
import { lineHashes, fmtRegion, HASH_SEP, MAX_HASH_LINES } from "./hashline";
|
|
14
14
|
import { toCwd } from "./paths";
|
|
15
|
-
import { abortIf,
|
|
16
|
-
import {
|
|
17
|
-
import { loadHashStore } from "./hash-store";
|
|
18
|
-
import { recordServed } from "./served";
|
|
15
|
+
import { abortIf, makePrepareArguments, visLines } from "./utils";
|
|
16
|
+
import { recordServedSafe } from "./served";
|
|
19
17
|
import { loadP, loadGuide } from "./prompts";
|
|
20
18
|
import { valAccess } from "./validation";
|
|
21
19
|
|
|
@@ -167,12 +165,7 @@ export function regRead(pi: ExtensionAPI): void {
|
|
|
167
165
|
description: R_DESC,
|
|
168
166
|
promptSnippet: R_SNIPPET,
|
|
169
167
|
promptGuidelines: readGuide(),
|
|
170
|
-
prepareArguments: (
|
|
171
|
-
if (!isRec(args)) return args as any;
|
|
172
|
-
const record = { ...args };
|
|
173
|
-
normalizeFilePath(record);
|
|
174
|
-
return record;
|
|
175
|
-
},
|
|
168
|
+
prepareArguments: makePrepareArguments(),
|
|
176
169
|
parameters: Type.Object({
|
|
177
170
|
path: Type.String({
|
|
178
171
|
description: "Path to the file to read (relative or absolute)",
|
|
@@ -223,18 +216,8 @@ export function regRead(pi: ExtensionAPI): void {
|
|
|
223
216
|
fileHashes,
|
|
224
217
|
resolvedPath,
|
|
225
218
|
);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
recordServed(store, resolvedPath, preview.servedHashes);
|
|
229
|
-
} catch (error) {
|
|
230
|
-
console.error("Failed to record served state from read:", error);
|
|
231
|
-
}
|
|
232
|
-
let snapshotId: string | undefined;
|
|
233
|
-
try {
|
|
234
|
-
snapshotId = (await fileSnap(absolutePath)).snapshotId;
|
|
235
|
-
} catch (error) {
|
|
236
|
-
console.error("Failed to compute snapshot for read:", error);
|
|
237
|
-
}
|
|
219
|
+
await recordServedSafe(resolvedPath, preview.servedHashes, "read");
|
|
220
|
+
const snapshotId = await safeSnapId(absolutePath, "read");
|
|
238
221
|
const previewText =
|
|
239
222
|
hadUtf8DecodeErrors
|
|
240
223
|
? `${preview.text}\n\n[Non-UTF-8 bytes shown as U+FFFD; editing rewrites the file as UTF-8.]`
|
package/src/replace-response.ts
CHANGED
|
@@ -101,7 +101,7 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
101
101
|
? `Replacement for ${noopEdit.loc} is identical to current content:\n ${noopEdit.loc}: ${clipLine(noopEdit.currentContent)}`
|
|
102
102
|
: "The edit produced identical content.";
|
|
103
103
|
|
|
104
|
-
const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}`;
|
|
104
|
+
const text = `No changes made to ${path}\nClassification: noop\n${noopDetailsText}${warnBlock(warnings)}`;
|
|
105
105
|
|
|
106
106
|
const metrics = buildMetrics({
|
|
107
107
|
classification: "noop",
|
package/src/replace-undo.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { contentChecksum } from "./hashline/hasher";
|
|
|
8
8
|
import { resolveTarget, writeAtomic } from "./fs-write";
|
|
9
9
|
import { toCwd } from "./paths";
|
|
10
10
|
import { toLF, stripBOM, genDiff, restoreEndings, type LineEnding } from "./replace-diff";
|
|
11
|
-
import { cntDiff, splitLines, errCode,
|
|
11
|
+
import { cntDiff, splitLines, errCode, makePrepareArguments } from "./utils";
|
|
12
12
|
import { loadP, loadGuide } from "./prompts";
|
|
13
13
|
import { buildMetrics } from "./replace-response";
|
|
14
14
|
import { changedRange, lineHashes } from "./hashline";
|
|
@@ -92,12 +92,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
92
92
|
description: loadP("../prompts/undo-last-replace.md"),
|
|
93
93
|
promptSnippet: loadP("../prompts/undo-last-replace-snippet.md"),
|
|
94
94
|
promptGuidelines: loadGuide("../prompts/undo-last-replace-guidelines.md"),
|
|
95
|
-
prepareArguments: (
|
|
96
|
-
if (!isRec(args)) return args as any;
|
|
97
|
-
const record = { ...args };
|
|
98
|
-
normalizeFilePath(record);
|
|
99
|
-
return record;
|
|
100
|
-
},
|
|
95
|
+
prepareArguments: makePrepareArguments(),
|
|
101
96
|
parameters: Type.Object({
|
|
102
97
|
path: Type.String({
|
|
103
98
|
description: "Path to the file to undo",
|
package/src/replace.ts
CHANGED
|
@@ -11,9 +11,9 @@ import {
|
|
|
11
11
|
restoreEndings,
|
|
12
12
|
type LineEnding,
|
|
13
13
|
} from "./replace-diff";
|
|
14
|
-
import { readNormFile } from "./file-reader";
|
|
14
|
+
import { readNormFile, safeSnapId } from "./file-reader";
|
|
15
15
|
import { normReq } from "./replace-normalize";
|
|
16
|
-
import { isRec, rejectUnknownFields, abortIf,
|
|
16
|
+
import { isRec, rejectUnknownFields, abortIf, makePrepareArguments } from "./utils";
|
|
17
17
|
import { resolveTarget, writeAtomic } from "./fs-write";
|
|
18
18
|
import { applyEdit,
|
|
19
19
|
lineHashes,
|
|
@@ -26,7 +26,6 @@ import { applyEdit,
|
|
|
26
26
|
type NEdit,
|
|
27
27
|
} from "./hashline";
|
|
28
28
|
import { toCwd } from "./paths";
|
|
29
|
-
import { fileSnap } from "./file-reader";
|
|
30
29
|
import {
|
|
31
30
|
buildChanged,
|
|
32
31
|
buildNoop,
|
|
@@ -47,7 +46,7 @@ import {
|
|
|
47
46
|
import { loadP, loadGuide } from "./prompts";
|
|
48
47
|
import { saveUndo } from "./replace-undo";
|
|
49
48
|
import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
|
|
50
|
-
import { getServed,
|
|
49
|
+
import { getServed, recordServedSafe, recordServedDiffSafe } from "./served";
|
|
51
50
|
|
|
52
51
|
const replacementTextSchema = Type.String({
|
|
53
52
|
description:
|
|
@@ -249,17 +248,9 @@ export async function execPipeline(
|
|
|
249
248
|
} catch (error) {
|
|
250
249
|
if (options?.noPersist !== true) {
|
|
251
250
|
if (error instanceof RangeStaleError) {
|
|
252
|
-
|
|
253
|
-
recordServed(hashStore, absolutePath, error.rangeHashes);
|
|
254
|
-
} catch (recordError) {
|
|
255
|
-
console.error("Failed to record served state from range-stale feedback:", recordError);
|
|
256
|
-
}
|
|
251
|
+
await recordServedSafe(absolutePath, error.rangeHashes, "range-stale feedback");
|
|
257
252
|
} else if (error instanceof AnchorMismatchError) {
|
|
258
|
-
|
|
259
|
-
recordServed(hashStore, absolutePath, error.feedbackHashes);
|
|
260
|
-
} catch (recordError) {
|
|
261
|
-
console.error("Failed to record served state from anchor-mismatch feedback:", recordError);
|
|
262
|
-
}
|
|
253
|
+
await recordServedSafe(absolutePath, error.feedbackHashes, "anchor-mismatch feedback");
|
|
263
254
|
}
|
|
264
255
|
}
|
|
265
256
|
throw error;
|
|
@@ -361,12 +352,7 @@ export function buildToolDef(): ToolDef {
|
|
|
361
352
|
parameters,
|
|
362
353
|
promptSnippet: E_SNIPPET,
|
|
363
354
|
promptGuidelines: E_GUIDE,
|
|
364
|
-
prepareArguments: (
|
|
365
|
-
if (!isRec(args)) return args as any;
|
|
366
|
-
const record = { ...args };
|
|
367
|
-
normalizeFilePath(record);
|
|
368
|
-
return record;
|
|
369
|
-
},
|
|
355
|
+
prepareArguments: makePrepareArguments(),
|
|
370
356
|
renderShell: "default",
|
|
371
357
|
renderCall(args, theme, context) {
|
|
372
358
|
const previewInput = getPreviewInput(args);
|
|
@@ -512,12 +498,7 @@ export function buildToolDef(): ToolDef {
|
|
|
512
498
|
|
|
513
499
|
const editsAttempted = 1;
|
|
514
500
|
if (originalNormalized === result) {
|
|
515
|
-
|
|
516
|
-
try {
|
|
517
|
-
noopSnapshotId = (await fileSnap(absolutePath)).snapshotId;
|
|
518
|
-
} catch (error) {
|
|
519
|
-
console.error("Failed to compute snapshot for noop edit:", error);
|
|
520
|
-
}
|
|
501
|
+
const noopSnapshotId = await safeSnapId(absolutePath, "noop edit");
|
|
521
502
|
return buildNoop({
|
|
522
503
|
path,
|
|
523
504
|
noopEdit,
|
|
@@ -561,12 +542,7 @@ export function buildToolDef(): ToolDef {
|
|
|
561
542
|
await undo.restore();
|
|
562
543
|
throw error;
|
|
563
544
|
}
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
updatedSnapshotId = (await fileSnap(absolutePath)).snapshotId;
|
|
567
|
-
} catch (error) {
|
|
568
|
-
console.error("Failed to compute post-edit snapshot:", error);
|
|
569
|
-
}
|
|
545
|
+
const updatedSnapshotId = await safeSnapId(absolutePath, "post-edit");
|
|
570
546
|
|
|
571
547
|
const editMeta: RMeta = {
|
|
572
548
|
editsAttempted,
|
|
@@ -589,12 +565,7 @@ export function buildToolDef(): ToolDef {
|
|
|
589
565
|
};
|
|
590
566
|
const changed = buildChanged(successInput);
|
|
591
567
|
if (changed.details.diff) {
|
|
592
|
-
|
|
593
|
-
const store = await loadHashStore();
|
|
594
|
-
recordServedDiff(store, mutationTargetPath, changed.details.diff);
|
|
595
|
-
} catch (error) {
|
|
596
|
-
console.error("Failed to record served state from post-edit diff:", error);
|
|
597
|
-
}
|
|
568
|
+
await recordServedDiffSafe(mutationTargetPath, changed.details.diff, "post-edit diff");
|
|
598
569
|
}
|
|
599
570
|
return changed;
|
|
600
571
|
});
|
package/src/served.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { parseHashList } from "./hash-store";
|
|
1
|
+
import { loadHashStore, parseHashList, type HashStore } from "./hash-store";
|
|
3
2
|
import { HASH_CLASS } from "./hashline/alphabet";
|
|
4
3
|
|
|
5
4
|
const SERVED_DIFF_ROW_RE = new RegExp(`^[+ ](${HASH_CLASS})│`);
|
|
@@ -42,3 +41,26 @@ export function recordServedDiff(store: HashStore, path: string, diff: string):
|
|
|
42
41
|
export function clearServed(store: HashStore, path: string): void {
|
|
43
42
|
store.stmts.servedDelete(path);
|
|
44
43
|
}
|
|
44
|
+
|
|
45
|
+
export async function recordServedSafe(
|
|
46
|
+
path: string,
|
|
47
|
+
hashes: string[],
|
|
48
|
+
context: string,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
if (hashes.length === 0) return;
|
|
51
|
+
try {
|
|
52
|
+
const store = await loadHashStore();
|
|
53
|
+
recordServed(store, path, hashes);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`Failed to record served state (${context}):`, error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function recordServedDiffSafe(
|
|
60
|
+
path: string,
|
|
61
|
+
diff: string,
|
|
62
|
+
context: string,
|
|
63
|
+
): Promise<void> {
|
|
64
|
+
if (!diff) return;
|
|
65
|
+
await recordServedSafe(path, servedHashesFromDiff(diff), context);
|
|
66
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -9,6 +9,15 @@ export function normalizeFilePath(record: Record<string, unknown>): void {
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function makePrepareArguments(): (args: unknown) => any {
|
|
13
|
+
return (args) => {
|
|
14
|
+
if (!isRec(args)) return args;
|
|
15
|
+
const record = { ...args };
|
|
16
|
+
normalizeFilePath(record);
|
|
17
|
+
return record;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export function splitLines(text: string): string[] {
|
|
13
22
|
if (text.length === 0) return [""];
|
|
14
23
|
const lines = text.split("\n");
|