@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,94 +0,0 @@
1
- import type { Anchor, HashlineCursor } from "./types";
2
- /**
3
- * Split a hashline diff into individual lines without losing the trailing
4
- * empty line that callers may rely on for explicit blank payloads. CRLF pairs
5
- * are normalized to a single line break.
6
- *
7
- * This mirrors the line-splitting performed by {@link HashlineTokenizer}'s
8
- * streaming drain loop and is kept for non-streaming callers that prefer
9
- * a single-shot split.
10
- */
11
- export declare function splitHashlineLines(text: string): string[];
12
- export declare function cloneCursor(cursor: HashlineCursor): HashlineCursor;
13
- /** Parse a bare line-number anchor (used by insert ops). Throws on malformed input. */
14
- export declare function parseLid(raw: string, lineNum: number): Anchor;
15
- export interface ParsedRange {
16
- start: Anchor;
17
- end: Anchor;
18
- }
19
- /**
20
- * Returns true when the line scans as `LINE!payload` (delete sigil followed by
21
- * additional content). The executor uses this for the dedicated "deletes only"
22
- * diagnostic, separate from the standard "unrecognized op" path.
23
- */
24
- export declare function isDeleteOpWithPayload(line: string): boolean;
25
- interface TokenBase {
26
- /** 1-indexed line number in the original input stream. */
27
- lineNum: number;
28
- }
29
- export type HashlineToken = (TokenBase & {
30
- kind: "blank";
31
- }) | (TokenBase & {
32
- kind: "envelope-begin";
33
- }) | (TokenBase & {
34
- kind: "envelope-end";
35
- }) | (TokenBase & {
36
- kind: "abort";
37
- }) | (TokenBase & {
38
- kind: "header";
39
- path: string;
40
- fileHash?: string;
41
- }) | (TokenBase & {
42
- kind: "op-insert";
43
- cursor: HashlineCursor;
44
- inlineBody: string | undefined;
45
- }) | (TokenBase & {
46
- kind: "op-replace";
47
- range: ParsedRange;
48
- inlineBody: string | undefined;
49
- }) | (TokenBase & {
50
- kind: "op-delete";
51
- range: ParsedRange;
52
- trailingPayload: boolean;
53
- }) | (TokenBase & {
54
- kind: "payload";
55
- text: string;
56
- }) | (TokenBase & {
57
- kind: "raw";
58
- text: string;
59
- });
60
- /**
61
- * Stateful, line-oriented classifier for hashline diff text. Use the streaming
62
- * {@link feed}/{@link end} pair to ingest text in chunks (each completed line
63
- * emits exactly one token; a trailing partial line stays buffered until the
64
- * next chunk or {@link end}). Use the stateless {@link tokenize}/predicate
65
- * methods for callers that already hold whole lines and only need
66
- * classification without buffering.
67
- */
68
- export declare class HashlineTokenizer {
69
- #private;
70
- /**
71
- * Ingest a chunk of input text. Each newline-terminated line in the
72
- * combined buffer produces one token. A trailing partial line (no `\n`
73
- * yet, possibly ending in a lone `\r`) stays buffered until the next
74
- * `feed`/`end` call so CRLF pairs that straddle chunk boundaries are
75
- * still normalized correctly.
76
- */
77
- feed(chunk: string): HashlineToken[];
78
- /**
79
- * Flush any buffered residual line (the last line of input when it lacks
80
- * a trailing newline) and mark the tokenizer closed. Calling `end` a
81
- * second time returns `[]`; reuse requires `reset`.
82
- */
83
- end(): HashlineToken[];
84
- /** Discard any buffered text and reset the line counter to 1. */
85
- reset(): void;
86
- /** Convenience: feed an entire text and immediately flush. */
87
- tokenizeAll(text: string): HashlineToken[];
88
- /** Stateless one-shot classification. Does not touch the streaming buffer. */
89
- tokenize(line: string, lineNum?: number): HashlineToken;
90
- isOp(line: string): boolean;
91
- isHeader(line: string): boolean;
92
- isEnvelopeMarker(line: string): boolean;
93
- }
94
- export {};
@@ -1,75 +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
- export type Anchor = {
6
- line: number;
7
- };
8
- export type HashlineCursor = {
9
- kind: "bof";
10
- } | {
11
- kind: "eof";
12
- } | {
13
- kind: "before_anchor";
14
- anchor: Anchor;
15
- } | {
16
- kind: "after_anchor";
17
- anchor: Anchor;
18
- };
19
- export type HashlineEdit = {
20
- kind: "insert";
21
- cursor: HashlineCursor;
22
- text: string;
23
- lineNum: number;
24
- index: number;
25
- } | {
26
- kind: "delete";
27
- anchor: Anchor;
28
- lineNum: number;
29
- index: number;
30
- oldAssertion?: string;
31
- };
32
- export interface HashlineInputSection {
33
- path: string;
34
- fileHash?: string;
35
- diff: string;
36
- }
37
- /** `path` is accepted by the edit tool runtime; other extra keys are preserved. */
38
- export declare const hashlineEditParamsSchema: z.ZodObject<{
39
- input: z.ZodString;
40
- path: z.ZodOptional<z.ZodString>;
41
- }, z.core.$loose>;
42
- export type HashlineParams = z.infer<typeof hashlineEditParamsSchema>;
43
- export interface HashlineStreamOptions {
44
- /** First line number to use when formatting (1-indexed). */
45
- startLine?: number;
46
- /** Maximum formatted lines per yielded chunk (default: 200). */
47
- maxChunkLines?: number;
48
- /** Maximum UTF-8 bytes per yielded chunk (default: 64 KiB). */
49
- maxChunkBytes?: number;
50
- }
51
- export interface CompactHashlineDiffPreview {
52
- preview: string;
53
- addedLines: number;
54
- removedLines: number;
55
- }
56
- export interface CompactHashlineDiffOptions {
57
- /** Maximum entries kept on each side of an unchanged-context truncation (default: 2). */
58
- maxUnchangedRun?: number;
59
- }
60
- export interface HashlineApplyOptions {
61
- autoDropPureInsertDuplicates?: boolean;
62
- }
63
- export interface SplitHashlineOptions {
64
- cwd?: string;
65
- path?: string;
66
- }
67
- export interface ExecuteHashlineSingleOptions {
68
- session: ToolSession;
69
- input: string;
70
- path?: string;
71
- signal?: AbortSignal;
72
- batchRequest?: LspBatchRequest;
73
- writethrough: WritethroughCallback;
74
- beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
75
- }
@@ -1,138 +0,0 @@
1
- /**
2
- * Per-session cache of file contents as they were rendered to the model by
3
- * the `read` and `search` tools in the current agent session.
4
- *
5
- * Used by hashline-mode anchor-stale recovery: if the model authored anchors
6
- * against a version of the file that no longer matches what is on disk —
7
- * because a subagent, the user, a linter, or a formatter modified the file
8
- * between the read and the edit — we replay the edits against the cached
9
- * pre-edit snapshot and 3-way-merge the result onto the live file.
10
- *
11
- * Scoped per `ToolSession`: the cache lives on the session object itself, so
12
- * different sessions never share snapshots and entries get reclaimed when
13
- * the session goes out of scope. Each session keeps a small LRU window of
14
- * paths; each path keeps a short ring of recent snapshots so follow-up edits
15
- * can recover from the agent's own prior writes as well as stale reads.
16
- */
17
- import { LRUCache } from "lru-cache/raw";
18
- import type { ToolSession } from "../tools";
19
-
20
- const MAX_PATHS_PER_SESSION = 30;
21
- const MAX_SNAPSHOTS_PER_PATH = 4;
22
-
23
- export interface FileReadSnapshot {
24
- /** 1-indexed line number → exact line content as observed by `read`/`search`. */
25
- lines: Map<number, string>;
26
- /** Full normalized text when the read path observed the whole file. */
27
- fullText?: string;
28
- /** 4-hex hash of `fullText`, or a sparse snapshot hash supplied by search. */
29
- fileHash?: string;
30
- recordedAt: number;
31
- }
32
-
33
- interface FileReadSnapshotMetadata {
34
- fullText?: string;
35
- fileHash?: string;
36
- }
37
-
38
- export class FileReadCache {
39
- #snapshots = new LRUCache<string, FileReadSnapshot[]>({ max: MAX_PATHS_PER_SESSION });
40
-
41
- /** Look up the most recent snapshot for `absPath`, or `null` if absent. */
42
- get(absPath: string): FileReadSnapshot | null {
43
- return this.#snapshots.get(absPath)?.[0] ?? null;
44
- }
45
-
46
- /** Look up the most recent snapshot for `absPath` whose file hash matches. */
47
- getByHash(absPath: string, fileHash: string): FileReadSnapshot | null {
48
- const history = this.#snapshots.get(absPath);
49
- return history?.find(snapshot => snapshot.fileHash === fileHash) ?? null;
50
- }
51
-
52
- /** Record a contiguous run of lines (e.g. from a `read` tool). `startLine` is 1-indexed. */
53
- recordContiguous(
54
- absPath: string,
55
- startLine: number,
56
- lines: readonly string[],
57
- metadata: FileReadSnapshotMetadata = {},
58
- ): void {
59
- if (lines.length === 0 && metadata.fullText === undefined) return;
60
- const entries: Array<readonly [number, string]> = lines.map((line, idx) => [startLine + idx, line] as const);
61
- this.#record(absPath, entries, metadata);
62
- }
63
-
64
- /** Record sparse `(lineNumber, content)` pairs (e.g. `search` matches plus context). */
65
- recordSparse(
66
- absPath: string,
67
- entries: Iterable<readonly [number, string]>,
68
- metadata: FileReadSnapshotMetadata = {},
69
- ): void {
70
- const arr = Array.from(entries);
71
- if (arr.length === 0 && metadata.fullText === undefined) return;
72
- this.#record(absPath, arr, metadata);
73
- }
74
-
75
- /** Drop the snapshot history for a single path. */
76
- invalidate(absPath: string): void {
77
- this.#snapshots.delete(absPath);
78
- }
79
-
80
- /** Drop every snapshot history. */
81
- clear(): void {
82
- this.#snapshots.clear();
83
- }
84
-
85
- #record(
86
- absPath: string,
87
- entries: ReadonlyArray<readonly [number, string]>,
88
- metadata: FileReadSnapshotMetadata,
89
- ): void {
90
- const history = this.#snapshots.get(absPath) ?? [];
91
- const head = history[0];
92
- const now = Date.now();
93
- if (head && !hasConflict(head.lines, entries) && !hasHashConflict(head, metadata)) {
94
- for (const [lineNum, content] of entries) head.lines.set(lineNum, content);
95
- if (metadata.fullText !== undefined) head.fullText = metadata.fullText;
96
- if (metadata.fileHash !== undefined) head.fileHash = metadata.fileHash;
97
- head.recordedAt = now;
98
- // `get` above already touched LRU recency for this key.
99
- return;
100
- }
101
-
102
- const nextSnapshot: FileReadSnapshot = {
103
- lines: new Map(entries),
104
- ...metadata,
105
- recordedAt: now,
106
- };
107
- const dedupedHistory = history.filter(snapshot => !isSameSnapshotIdentity(snapshot, nextSnapshot));
108
- this.#snapshots.set(absPath, [nextSnapshot, ...dedupedHistory].slice(0, MAX_SNAPSHOTS_PER_PATH));
109
- }
110
- }
111
-
112
- function hasConflict(existing: Map<number, string>, incoming: ReadonlyArray<readonly [number, string]>): boolean {
113
- for (const [lineNum, content] of incoming) {
114
- const prior = existing.get(lineNum);
115
- if (prior !== undefined && prior !== content) return true;
116
- }
117
- return false;
118
- }
119
-
120
- function hasHashConflict(existing: FileReadSnapshot, metadata: FileReadSnapshotMetadata): boolean {
121
- return metadata.fileHash !== undefined && existing.fileHash !== undefined && metadata.fileHash !== existing.fileHash;
122
- }
123
-
124
- function isSameSnapshotIdentity(left: FileReadSnapshot, right: FileReadSnapshot): boolean {
125
- if (left.fileHash !== undefined && right.fileHash !== undefined) return left.fileHash === right.fileHash;
126
- if (left.fullText !== undefined && right.fullText !== undefined) return left.fullText === right.fullText;
127
- return false;
128
- }
129
-
130
- /**
131
- * Look up (or lazily create) the file-read cache attached to a session. The
132
- * cache is stored as `session.fileReadCache` so it lives exactly as long as
133
- * the session itself.
134
- */
135
- export function getFileReadCache(session: ToolSession): FileReadCache {
136
- if (!session.fileReadCache) session.fileReadCache = new FileReadCache();
137
- return session.fileReadCache;
138
- }
@@ -1,104 +0,0 @@
1
- import { MISMATCH_CONTEXT } from "./constants";
2
- import { formatNumberedLine, HL_FILE_HASH_SEP, HL_FILE_PREFIX } from "./hash";
3
-
4
- const LINE_REF_RE = /^\s*[>+\-*]*\s*(\d+)(?::.*)?\s*$/;
5
-
6
- export function formatFullAnchorRequirement(raw?: string): string {
7
- const received = raw === undefined ? "" : ` Received ${JSON.stringify(raw)}.`;
8
- return (
9
- `a bare line number from read/search output plus the section header file hash ` +
10
- `(for example ${HL_FILE_PREFIX}src/foo.ts${HL_FILE_HASH_SEP}1a2b and line "160")${received}`
11
- );
12
- }
13
-
14
- export function parseTag(ref: string): { line: number } {
15
- const match = ref.match(LINE_REF_RE);
16
- if (!match) {
17
- throw new Error(`Invalid line reference. Expected ${formatFullAnchorRequirement(ref)}.`);
18
- }
19
- const line = Number.parseInt(match[1], 10);
20
- if (line < 1) throw new Error(`Line number must be >= 1, got ${line} in "${ref}".`);
21
- return { line };
22
- }
23
-
24
- export interface HashlineMismatchDetails {
25
- path?: string;
26
- expectedFileHash: string;
27
- actualFileHash: string;
28
- fileLines: string[];
29
- anchorLines?: readonly number[];
30
- }
31
-
32
- function getMismatchDisplayLines(anchorLines: readonly number[], fileLines: string[]): number[] {
33
- const displayLines = new Set<number>();
34
- for (const line of anchorLines) {
35
- if (line < 1 || line > fileLines.length) continue;
36
- const lo = Math.max(1, line - MISMATCH_CONTEXT);
37
- const hi = Math.min(fileLines.length, line + MISMATCH_CONTEXT);
38
- for (let lineNum = lo; lineNum <= hi; lineNum++) displayLines.add(lineNum);
39
- }
40
- return [...displayLines].sort((a, b) => a - b);
41
- }
42
-
43
- export class HashlineMismatchError extends Error {
44
- readonly path: string | undefined;
45
- readonly expectedFileHash: string;
46
- readonly actualFileHash: string;
47
- readonly fileLines: string[];
48
- readonly anchorLines: readonly number[];
49
-
50
- constructor(details: HashlineMismatchDetails) {
51
- super(HashlineMismatchError.formatMessage(details));
52
- this.name = "HashlineMismatchError";
53
- this.path = details.path;
54
- this.expectedFileHash = details.expectedFileHash;
55
- this.actualFileHash = details.actualFileHash;
56
- this.fileLines = details.fileLines;
57
- this.anchorLines = details.anchorLines ?? [];
58
- }
59
-
60
- get displayMessage(): string {
61
- return HashlineMismatchError.formatDisplayMessage({
62
- path: this.path,
63
- expectedFileHash: this.expectedFileHash,
64
- actualFileHash: this.actualFileHash,
65
- fileLines: this.fileLines,
66
- anchorLines: this.anchorLines,
67
- });
68
- }
69
-
70
- static rejectionHeader(details: HashlineMismatchDetails): string[] {
71
- const pathText = details.path ? ` for ${details.path}` : "";
72
- return [
73
- `Edit rejected${pathText}: file changed between read and edit.`,
74
- `Section is bound to ${HL_FILE_HASH_SEP}${details.expectedFileHash}, but the current file hashes to ${HL_FILE_HASH_SEP}${details.actualFileHash}. If your previous edit in this session modified this file, copy the ${HL_FILE_PREFIX}path${HL_FILE_HASH_SEP}newhash from that edit's response. Otherwise re-read the file before retrying.`,
75
- ];
76
- }
77
-
78
- static formatDisplayMessage(details: HashlineMismatchDetails): string {
79
- return HashlineMismatchError.formatMessage(details);
80
- }
81
-
82
- static formatMessage(details: HashlineMismatchDetails): string {
83
- const anchorSet = new Set(details.anchorLines ?? []);
84
- const lines = HashlineMismatchError.rejectionHeader(details);
85
- const displayLines = getMismatchDisplayLines(details.anchorLines ?? [], details.fileLines);
86
- if (displayLines.length === 0) return lines.join("\n");
87
- lines.push("");
88
- let previous = -1;
89
- for (const lineNum of displayLines) {
90
- if (previous !== -1 && lineNum > previous + 1) lines.push("...");
91
- previous = lineNum;
92
- const text = details.fileLines[lineNum - 1] ?? "";
93
- const marker = anchorSet.has(lineNum) ? "*" : " ";
94
- lines.push(`${marker}${formatNumberedLine(lineNum, text)}`);
95
- }
96
- return lines.join("\n");
97
- }
98
- }
99
-
100
- export function validateLineRef(ref: { line: number }, fileLines: string[]): void {
101
- if (ref.line < 1 || ref.line > fileLines.length) {
102
- throw new Error(`Line ${ref.line} does not exist (file has ${fileLines.length} lines)`);
103
- }
104
- }