@oh-my-pi/pi-coding-agent 15.5.2 → 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 (77) hide show
  1. package/CHANGELOG.md +38 -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/bash.d.ts +1 -0
  14. package/dist/types/tools/index.d.ts +6 -5
  15. package/dist/types/tools/path-utils.d.ts +18 -0
  16. package/dist/types/utils/changelog.d.ts +8 -3
  17. package/package.json +8 -15
  18. package/src/config/settings-schema.ts +32 -0
  19. package/src/config.ts +42 -15
  20. package/src/edit/file-snapshot-store.ts +22 -0
  21. package/src/edit/hashline/diff.ts +88 -0
  22. package/src/edit/hashline/execute.ts +188 -0
  23. package/src/edit/hashline/filesystem.ts +129 -0
  24. package/src/edit/hashline/index.ts +4 -0
  25. package/src/edit/hashline/params.ts +11 -0
  26. package/src/edit/index.ts +7 -15
  27. package/src/edit/normalize.ts +11 -41
  28. package/src/edit/renderer.ts +1 -1
  29. package/src/edit/streaming.ts +8 -9
  30. package/src/index.ts +0 -1
  31. package/src/internal-urls/docs-index.generated.ts +1 -1
  32. package/src/sdk.ts +8 -1
  33. package/src/tools/ast-edit.ts +1 -1
  34. package/src/tools/ast-grep.ts +3 -3
  35. package/src/tools/bash.ts +74 -10
  36. package/src/tools/index.ts +6 -5
  37. package/src/tools/path-utils.ts +81 -0
  38. package/src/tools/read.ts +14 -72
  39. package/src/tools/search.ts +136 -17
  40. package/src/tools/write.ts +3 -3
  41. package/src/utils/changelog.ts +11 -3
  42. package/src/utils/file-mentions.ts +1 -1
  43. package/dist/types/edit/file-read-cache.d.ts +0 -36
  44. package/dist/types/hashline/anchors.d.ts +0 -26
  45. package/dist/types/hashline/apply.d.ts +0 -14
  46. package/dist/types/hashline/constants.d.ts +0 -40
  47. package/dist/types/hashline/diff-preview.d.ts +0 -2
  48. package/dist/types/hashline/diff.d.ts +0 -16
  49. package/dist/types/hashline/execute.d.ts +0 -4
  50. package/dist/types/hashline/executor.d.ts +0 -56
  51. package/dist/types/hashline/hash.d.ts +0 -76
  52. package/dist/types/hashline/index.d.ts +0 -14
  53. package/dist/types/hashline/input.d.ts +0 -4
  54. package/dist/types/hashline/prefixes.d.ts +0 -7
  55. package/dist/types/hashline/recovery.d.ts +0 -21
  56. package/dist/types/hashline/stream.d.ts +0 -2
  57. package/dist/types/hashline/tokenizer.d.ts +0 -94
  58. package/dist/types/hashline/types.d.ts +0 -75
  59. package/src/edit/file-read-cache.ts +0 -138
  60. package/src/hashline/anchors.ts +0 -104
  61. package/src/hashline/apply.ts +0 -790
  62. package/src/hashline/bigrams.json +0 -649
  63. package/src/hashline/constants.ts +0 -51
  64. package/src/hashline/diff-preview.ts +0 -42
  65. package/src/hashline/diff.ts +0 -82
  66. package/src/hashline/execute.ts +0 -334
  67. package/src/hashline/executor.ts +0 -334
  68. package/src/hashline/grammar.lark +0 -23
  69. package/src/hashline/hash.ts +0 -131
  70. package/src/hashline/index.ts +0 -14
  71. package/src/hashline/input.ts +0 -137
  72. package/src/hashline/prefixes.ts +0 -111
  73. package/src/hashline/recovery.ts +0 -139
  74. package/src/hashline/stream.ts +0 -123
  75. package/src/hashline/tokenizer.ts +0 -473
  76. package/src/hashline/types.ts +0 -66
  77. package/src/prompts/tools/hashline.md +0 -63
@@ -1,111 +0,0 @@
1
- const HL_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:[+*-]\s*)?\d+:/;
2
- const HL_PREFIX_PLUS_RE = /^\s*(?:>>>|>>)?\s*\+\s*\d+:/;
3
- const HL_HEADER_RE = /^\s*¶\S+#[0-9a-f]{4}\s*$/;
4
- const DIFF_PLUS_RE = /^[+](?![+])/;
5
- const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bUse :L?\d+/;
6
-
7
- function stripLeadingHashlinePrefixes(line: string): string {
8
- let result = line;
9
- let previous: string;
10
- do {
11
- previous = result;
12
- result = result.replace(HL_PREFIX_RE, "");
13
- } while (result !== previous);
14
- return result;
15
- }
16
-
17
- // ───────────────────────────────────────────────────────────────────────────
18
- // 5. Read-output prefix stripping
19
- //
20
- // When a model echoes back content from a `read` or `search` response, every
21
- // line is prefixed with either a hashline-mode line number (`123:`) or, for
22
- // diff-style echoes, a leading `+`. These helpers detect that and recover the
23
- // raw text.
24
- // ───────────────────────────────────────────────────────────────────────────
25
-
26
- type LinePrefixStats = {
27
- nonEmpty: number;
28
- headerCount: number;
29
- hashPrefixCount: number;
30
- diffPlusHashPrefixCount: number;
31
- diffPlusCount: number;
32
- truncationNoticeCount: number;
33
- };
34
-
35
- function collectLinePrefixStats(lines: string[]): LinePrefixStats {
36
- const stats: LinePrefixStats = {
37
- nonEmpty: 0,
38
- headerCount: 0,
39
- hashPrefixCount: 0,
40
- diffPlusHashPrefixCount: 0,
41
- diffPlusCount: 0,
42
- truncationNoticeCount: 0,
43
- };
44
-
45
- for (const line of lines) {
46
- if (line.length === 0) continue;
47
- if (READ_TRUNCATION_NOTICE_RE.test(line)) {
48
- stats.truncationNoticeCount++;
49
- continue;
50
- }
51
- if (HL_HEADER_RE.test(line)) {
52
- stats.nonEmpty++;
53
- stats.headerCount++;
54
- continue;
55
- }
56
- stats.nonEmpty++;
57
- if (HL_PREFIX_RE.test(line)) stats.hashPrefixCount++;
58
- if (HL_PREFIX_PLUS_RE.test(line)) stats.diffPlusHashPrefixCount++;
59
- if (DIFF_PLUS_RE.test(line)) stats.diffPlusCount++;
60
- }
61
- return stats;
62
- }
63
-
64
- export function stripNewLinePrefixes(lines: string[]): string[] {
65
- const stats = collectLinePrefixStats(lines);
66
- if (stats.nonEmpty === 0) return lines;
67
-
68
- const contentLineCount = stats.nonEmpty - stats.headerCount;
69
- const stripHash = contentLineCount > 0 && stats.hashPrefixCount === contentLineCount;
70
- const stripPlus =
71
- !stripHash &&
72
- stats.diffPlusHashPrefixCount === 0 &&
73
- stats.diffPlusCount > 0 &&
74
- stats.diffPlusCount >= stats.nonEmpty * 0.5;
75
-
76
- if (!stripHash && !stripPlus && stats.diffPlusHashPrefixCount === 0) return lines;
77
-
78
- return lines
79
- .filter(line => !READ_TRUNCATION_NOTICE_RE.test(line) && !(stripHash && HL_HEADER_RE.test(line)))
80
- .map(line => {
81
- if (stripHash) return stripLeadingHashlinePrefixes(line);
82
- if (stripPlus) return line.replace(DIFF_PLUS_RE, "");
83
- if (stats.diffPlusHashPrefixCount > 0 && HL_PREFIX_PLUS_RE.test(line)) {
84
- return line.replace(HL_PREFIX_RE, "");
85
- }
86
- return line;
87
- });
88
- }
89
-
90
- export function stripHashlinePrefixes(lines: string[]): string[] {
91
- const stats = collectLinePrefixStats(lines);
92
- if (stats.nonEmpty === 0) return lines;
93
- const contentLineCount = stats.nonEmpty - stats.headerCount;
94
- if (contentLineCount === 0 || stats.hashPrefixCount !== contentLineCount) return lines;
95
- return lines
96
- .filter(line => !READ_TRUNCATION_NOTICE_RE.test(line) && !HL_HEADER_RE.test(line))
97
- .map(line => stripLeadingHashlinePrefixes(line));
98
- }
99
-
100
- /**
101
- * Normalize line payloads by stripping read/search line prefixes. `null` /
102
- * `undefined` yield `[]`; a single multiline string is split on `\n`.
103
- */
104
- export function hashlineParseText(edit: string[] | string | null | undefined): string[] {
105
- if (edit == null) return [];
106
- if (typeof edit === "string") {
107
- const trimmed = edit.endsWith("\n") ? edit.slice(0, -1) : edit;
108
- edit = trimmed.replaceAll("\r", "").split("\n");
109
- }
110
- return stripNewLinePrefixes(edit);
111
- }
@@ -1,139 +0,0 @@
1
- import * as Diff from "diff";
2
- import { generateDiffString } from "../edit/diff";
3
- import type { FileReadCache, FileReadSnapshot } from "../edit/file-read-cache";
4
- import { applyHashlineEdits, type HashlineApplyResult } from "./apply";
5
- import { computeFileHash } from "./hash";
6
- import type { HashlineApplyOptions, HashlineEdit } from "./types";
7
-
8
- export interface HashlineRecoveryArgs {
9
- cache: FileReadCache;
10
- absolutePath: string;
11
- currentText: string;
12
- fileHash: string;
13
- edits: HashlineEdit[];
14
- options: HashlineApplyOptions;
15
- }
16
-
17
- export interface HashlineRecoveryResult {
18
- lines: string;
19
- firstChangedLine: number | undefined;
20
- warnings: string[];
21
- }
22
-
23
- // Section hashes are line-precise; never let Diff.applyPatch slide a hunk onto a
24
- // duplicate closer 100+ lines away. If snapshot replay does not align exactly,
25
- // refuse and let the model re-read.
26
- const HASHLINE_RECOVERY_FUZZ_FACTOR = 0;
27
-
28
- const HASHLINE_RECOVERY_EXTERNAL_WARNING =
29
- "Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
30
- const HASHLINE_RECOVERY_SESSION_CHAIN_WARNING =
31
- "Recovered from a stale file hash using an earlier in-session snapshot (the file hash advanced after a prior edit in this session).";
32
- const HASHLINE_RECOVERY_SESSION_REPLAY_WARNING =
33
- "Recovered by replaying your edits onto the current file content — your previous edit in this session changed line(s) you re-targeted with a stale hash. Verify the diff matches your intent before continuing.";
34
-
35
- function applyEditsToSnapshot(
36
- previousText: string,
37
- currentText: string,
38
- edits: HashlineEdit[],
39
- options: HashlineApplyOptions,
40
- recoveryWarning: string,
41
- ): HashlineRecoveryResult | null {
42
- let applied: HashlineApplyResult;
43
- try {
44
- applied = applyHashlineEdits(previousText, edits, options);
45
- } catch {
46
- return null;
47
- }
48
- if (applied.lines === previousText) return null;
49
-
50
- const patch = Diff.structuredPatch("file", "file", previousText, applied.lines, "", "", { context: 3 });
51
- const merged = Diff.applyPatch(currentText, patch, { fuzzFactor: HASHLINE_RECOVERY_FUZZ_FACTOR });
52
- if (typeof merged !== "string" || merged === currentText) return null;
53
-
54
- const mergedDiff = generateDiffString(currentText, merged);
55
- const hasNetChange = mergedDiff.firstChangedLine !== undefined;
56
- const recoveryWarnings = hasNetChange
57
- ? [recoveryWarning, ...(applied.warnings ?? [])]
58
- : [...(applied.warnings ?? [])];
59
-
60
- return {
61
- lines: merged,
62
- firstChangedLine: mergedDiff.firstChangedLine ?? applied.firstChangedLine,
63
- warnings: recoveryWarnings,
64
- };
65
- }
66
-
67
- function replaySessionChainOnCurrent(
68
- previousText: string,
69
- currentText: string,
70
- edits: HashlineEdit[],
71
- options: HashlineApplyOptions,
72
- ): HashlineRecoveryResult | null {
73
- // Only safe when no insert/delete shifted line counts in the prior edit
74
- // chain: if total line counts match, every line number in `edits` still
75
- // resolves to the same logical row.
76
- if (previousText.split("\n").length !== currentText.split("\n").length) return null;
77
- let applied: HashlineApplyResult;
78
- try {
79
- applied = applyHashlineEdits(currentText, edits, options);
80
- } catch {
81
- return null;
82
- }
83
- if (applied.lines === currentText) return null;
84
- return {
85
- lines: applied.lines,
86
- firstChangedLine: applied.firstChangedLine,
87
- warnings: [HASHLINE_RECOVERY_SESSION_REPLAY_WARNING, ...(applied.warnings ?? [])],
88
- };
89
- }
90
-
91
- function buildSparseOverlayText(currentText: string, snapshotLines: ReadonlyMap<number, string>): string {
92
- const overlaid = currentText.split("\n");
93
- let maxCachedLine = 0;
94
- for (const lineNum of snapshotLines.keys()) {
95
- if (lineNum > maxCachedLine) maxCachedLine = lineNum;
96
- }
97
- while (overlaid.length < maxCachedLine) overlaid.push("");
98
- for (const [lineNum, content] of snapshotLines) {
99
- overlaid[lineNum - 1] = content;
100
- }
101
- return overlaid.join("\n");
102
- }
103
-
104
- function isHeadSnapshot(head: FileReadSnapshot | null, snapshot: FileReadSnapshot): boolean {
105
- return head === snapshot;
106
- }
107
-
108
- function resolveRecoveryWarning(head: FileReadSnapshot | null, snapshot: FileReadSnapshot): string {
109
- return isHeadSnapshot(head, snapshot) ? HASHLINE_RECOVERY_EXTERNAL_WARNING : HASHLINE_RECOVERY_SESSION_CHAIN_WARNING;
110
- }
111
-
112
- /**
113
- * Attempt to recover from a section file-hash mismatch by replaying the edits
114
- * against a cached pre-edit snapshot of the file and 3-way-merging the result
115
- * onto the current on-disk content. Returns `null` when no recovery is possible.
116
- */
117
- export function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): HashlineRecoveryResult | null {
118
- const { cache, absolutePath, currentText, fileHash, edits, options } = args;
119
- const head = cache.get(absolutePath);
120
- const snapshot = cache.getByHash(absolutePath, fileHash);
121
- if (!snapshot || snapshot.lines.size === 0) return null;
122
-
123
- const recoveryWarning = resolveRecoveryWarning(head, snapshot);
124
- const isSessionChain = !isHeadSnapshot(head, snapshot);
125
- if (snapshot.fullText !== undefined) {
126
- const merged = applyEditsToSnapshot(snapshot.fullText, currentText, edits, options, recoveryWarning);
127
- if (merged !== null) return merged;
128
- // Session-chain fast-path: prior in-session edit changed the same line(s)
129
- // the model is now re-targeting with the stale hash. When line counts
130
- // match, the edits' line numbers still resolve to the right rows — replay
131
- // onto the current text directly.
132
- if (isSessionChain) return replaySessionChainOnCurrent(snapshot.fullText, currentText, edits, options);
133
- return null;
134
- }
135
-
136
- const overlayText = buildSparseOverlayText(currentText, snapshot.lines);
137
- if (computeFileHash(overlayText) !== fileHash) return null;
138
- return applyEditsToSnapshot(overlayText, currentText, edits, options, recoveryWarning);
139
- }
@@ -1,123 +0,0 @@
1
- import { formatNumberedLine } from "./hash";
2
- import type { HashlineStreamOptions } from "./types";
3
-
4
- interface ResolvedHashlineStreamOptions {
5
- startLine: number;
6
- maxChunkLines: number;
7
- maxChunkBytes: number;
8
- }
9
-
10
- function resolveHashlineStreamOptions(options: HashlineStreamOptions): ResolvedHashlineStreamOptions {
11
- return {
12
- startLine: options.startLine ?? 1,
13
- maxChunkLines: options.maxChunkLines ?? 200,
14
- maxChunkBytes: options.maxChunkBytes ?? 64 * 1024,
15
- };
16
- }
17
-
18
- interface HashlineChunkEmitter {
19
- pushLine: (line: string) => string[];
20
- flush: () => string | undefined;
21
- }
22
-
23
- function createHashlineChunkEmitter(options: ResolvedHashlineStreamOptions): HashlineChunkEmitter {
24
- let lineNumber = options.startLine;
25
- let outLines: string[] = [];
26
- let outBytes = 0;
27
-
28
- const flush = (): string | undefined => {
29
- if (outLines.length === 0) return undefined;
30
- const chunk = outLines.join("\n");
31
- outLines = [];
32
- outBytes = 0;
33
- return chunk;
34
- };
35
-
36
- const pushLine = (line: string): string[] => {
37
- const formatted = formatNumberedLine(lineNumber, line);
38
- lineNumber++;
39
-
40
- const chunks: string[] = [];
41
- const sepBytes = outLines.length === 0 ? 0 : 1;
42
- const lineBytes = Buffer.byteLength(formatted, "utf-8");
43
- const wouldOverflow =
44
- outLines.length >= options.maxChunkLines || outBytes + sepBytes + lineBytes > options.maxChunkBytes;
45
-
46
- if (outLines.length > 0 && wouldOverflow) {
47
- const flushed = flush();
48
- if (flushed) chunks.push(flushed);
49
- }
50
-
51
- outLines.push(formatted);
52
- outBytes += (outLines.length === 1 ? 0 : 1) + lineBytes;
53
-
54
- if (outLines.length >= options.maxChunkLines || outBytes >= options.maxChunkBytes) {
55
- const flushed = flush();
56
- if (flushed) chunks.push(flushed);
57
- }
58
- return chunks;
59
- };
60
-
61
- return { pushLine, flush };
62
- }
63
-
64
- function isReadableStream(value: unknown): value is ReadableStream<Uint8Array> {
65
- return (
66
- typeof value === "object" &&
67
- value !== null &&
68
- "getReader" in value &&
69
- typeof (value as { getReader?: unknown }).getReader === "function"
70
- );
71
- }
72
-
73
- async function* bytesFromReadableStream(stream: ReadableStream<Uint8Array>): AsyncGenerator<Uint8Array> {
74
- const reader = stream.getReader();
75
- try {
76
- while (true) {
77
- const { done, value } = await reader.read();
78
- if (done) return;
79
- if (value) yield value;
80
- }
81
- } finally {
82
- reader.releaseLock();
83
- }
84
- }
85
-
86
- export async function* streamHashLinesFromUtf8(
87
- source: ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>,
88
- options: HashlineStreamOptions = {},
89
- ): AsyncGenerator<string> {
90
- const resolved = resolveHashlineStreamOptions(options);
91
- const decoder = new TextDecoder("utf-8");
92
- const chunks = isReadableStream(source) ? bytesFromReadableStream(source) : source;
93
- const emitter = createHashlineChunkEmitter(resolved);
94
-
95
- let pending = "";
96
- let sawAnyLine = false;
97
-
98
- for await (const chunk of chunks) {
99
- pending += decoder.decode(chunk, { stream: true });
100
- let nl = pending.indexOf("\n");
101
- while (nl !== -1) {
102
- const raw = pending.slice(0, nl);
103
- const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
104
- sawAnyLine = true;
105
- for (const out of emitter.pushLine(line)) yield out;
106
- pending = pending.slice(nl + 1);
107
- nl = pending.indexOf("\n");
108
- }
109
- }
110
-
111
- pending += decoder.decode();
112
- if (pending.length > 0) {
113
- sawAnyLine = true;
114
- const tail = pending.endsWith("\r") ? pending.slice(0, -1) : pending;
115
- for (const out of emitter.pushLine(tail)) yield out;
116
- }
117
- if (!sawAnyLine) {
118
- for (const out of emitter.pushLine("")) yield out;
119
- }
120
-
121
- const last = emitter.flush();
122
- if (last) yield last;
123
- }