@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.
- package/CHANGELOG.md +38 -0
- package/dist/types/config/settings-schema.d.ts +27 -0
- package/dist/types/config.d.ts +31 -5
- package/dist/types/edit/file-snapshot-store.d.ts +18 -0
- package/dist/types/edit/hashline/diff.d.ts +30 -0
- package/dist/types/edit/hashline/execute.d.ts +29 -0
- package/dist/types/edit/hashline/filesystem.d.ts +57 -0
- package/dist/types/edit/hashline/index.d.ts +4 -0
- package/dist/types/edit/hashline/params.d.ts +12 -0
- package/dist/types/edit/index.d.ts +4 -3
- package/dist/types/edit/normalize.d.ts +4 -16
- package/dist/types/index.d.ts +0 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +18 -0
- package/dist/types/utils/changelog.d.ts +8 -3
- package/package.json +8 -15
- package/src/config/settings-schema.ts +32 -0
- package/src/config.ts +42 -15
- package/src/edit/file-snapshot-store.ts +22 -0
- package/src/edit/hashline/diff.ts +88 -0
- package/src/edit/hashline/execute.ts +188 -0
- package/src/edit/hashline/filesystem.ts +129 -0
- package/src/edit/hashline/index.ts +4 -0
- package/src/edit/hashline/params.ts +11 -0
- package/src/edit/index.ts +7 -15
- package/src/edit/normalize.ts +11 -41
- package/src/edit/renderer.ts +1 -1
- package/src/edit/streaming.ts +8 -9
- package/src/index.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/sdk.ts +8 -1
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +3 -3
- package/src/tools/bash.ts +74 -10
- package/src/tools/index.ts +6 -5
- package/src/tools/path-utils.ts +81 -0
- package/src/tools/read.ts +14 -72
- package/src/tools/search.ts +136 -17
- package/src/tools/write.ts +3 -3
- package/src/utils/changelog.ts +11 -3
- package/src/utils/file-mentions.ts +1 -1
- package/dist/types/edit/file-read-cache.d.ts +0 -36
- package/dist/types/hashline/anchors.d.ts +0 -26
- package/dist/types/hashline/apply.d.ts +0 -14
- package/dist/types/hashline/constants.d.ts +0 -40
- package/dist/types/hashline/diff-preview.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +0 -16
- package/dist/types/hashline/execute.d.ts +0 -4
- package/dist/types/hashline/executor.d.ts +0 -56
- package/dist/types/hashline/hash.d.ts +0 -76
- package/dist/types/hashline/index.d.ts +0 -14
- package/dist/types/hashline/input.d.ts +0 -4
- package/dist/types/hashline/prefixes.d.ts +0 -7
- package/dist/types/hashline/recovery.d.ts +0 -21
- package/dist/types/hashline/stream.d.ts +0 -2
- package/dist/types/hashline/tokenizer.d.ts +0 -94
- package/dist/types/hashline/types.d.ts +0 -75
- package/src/edit/file-read-cache.ts +0 -138
- package/src/hashline/anchors.ts +0 -104
- package/src/hashline/apply.ts +0 -790
- package/src/hashline/bigrams.json +0 -649
- package/src/hashline/constants.ts +0 -51
- package/src/hashline/diff-preview.ts +0 -42
- package/src/hashline/diff.ts +0 -82
- package/src/hashline/execute.ts +0 -334
- package/src/hashline/executor.ts +0 -334
- package/src/hashline/grammar.lark +0 -23
- package/src/hashline/hash.ts +0 -131
- package/src/hashline/index.ts +0 -14
- package/src/hashline/input.ts +0 -137
- package/src/hashline/prefixes.ts +0 -111
- package/src/hashline/recovery.ts +0 -139
- package/src/hashline/stream.ts +0 -123
- package/src/hashline/tokenizer.ts +0 -473
- package/src/hashline/types.ts +0 -66
- package/src/prompts/tools/hashline.md +0 -63
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { HashlineApplyOptions, HashlineInputSection } from "./types";
|
|
2
|
-
export declare function computeHashlineSectionDiff(section: HashlineInputSection, cwd: string, options?: HashlineApplyOptions): Promise<{
|
|
3
|
-
diff: string;
|
|
4
|
-
firstChangedLine: number | undefined;
|
|
5
|
-
} | {
|
|
6
|
-
error: string;
|
|
7
|
-
}>;
|
|
8
|
-
export declare function computeHashlineDiff(input: {
|
|
9
|
-
input: string;
|
|
10
|
-
path?: string;
|
|
11
|
-
}, cwd: string, options?: HashlineApplyOptions): Promise<{
|
|
12
|
-
diff: string;
|
|
13
|
-
firstChangedLine: number | undefined;
|
|
14
|
-
} | {
|
|
15
|
-
error: string;
|
|
16
|
-
}>;
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
-
import type { EditToolDetails } from "../edit/renderer";
|
|
3
|
-
import type { ExecuteHashlineSingleOptions, hashlineEditParamsSchema } from "./types";
|
|
4
|
-
export declare function executeHashlineSingle(options: ExecuteHashlineSingleOptions): Promise<AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema>>;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { type HashlineToken } from "./tokenizer";
|
|
2
|
-
import type { HashlineEdit } from "./types";
|
|
3
|
-
/**
|
|
4
|
-
* Token-driven state machine that turns a stream of {@link HashlineToken}s
|
|
5
|
-
* into the flat list of {@link HashlineEdit}s applied downstream by the
|
|
6
|
-
* apply/diff layers.
|
|
7
|
-
*
|
|
8
|
-
* The executor owns:
|
|
9
|
-
* - the running edit index (kept monotonic across pending flushes),
|
|
10
|
-
* - the pending-payload buffer (lines accumulated for the most recently
|
|
11
|
-
* opened insert/replace op),
|
|
12
|
-
* - all parse-time diagnostics (range order, "delete with payload",
|
|
13
|
-
* orphan payload, unrecognized op),
|
|
14
|
-
* - the {@link terminated} flag set by `envelope-end`/`abort`.
|
|
15
|
-
*
|
|
16
|
-
* Tokens are dispatched in the order they arrive; the matching tokenizer
|
|
17
|
-
* supplies the line numbers carried inside each token so diagnostics line
|
|
18
|
-
* up with the source.
|
|
19
|
-
*/
|
|
20
|
-
export declare class HashlineExecutor {
|
|
21
|
-
#private;
|
|
22
|
-
/** True once an `envelope-end` or `abort` token has been observed. */
|
|
23
|
-
get terminated(): boolean;
|
|
24
|
-
/**
|
|
25
|
-
* Consume one token. After `terminated` flips true subsequent feeds
|
|
26
|
-
* are silently ignored so callers can keep draining their tokenizer
|
|
27
|
-
* without explicit early-exit guards.
|
|
28
|
-
*/
|
|
29
|
-
feed(token: HashlineToken): void;
|
|
30
|
-
/**
|
|
31
|
-
* Flush any open pending op (with its full accumulated payload, including
|
|
32
|
-
* explicit `+` blank lines) and return the accumulated edits and warnings.
|
|
33
|
-
* The executor is single-use; reset() is required for reuse.
|
|
34
|
-
* Throws if two replace/delete ops target the same line with non-identical
|
|
35
|
-
* shapes (different ranges, replace+delete, delete+delete). Identical-range
|
|
36
|
-
* `A-B:` pairs in the same hunk are coalesced last-wins by `feed()` with a
|
|
37
|
-
* warning, so they never reach the validator.
|
|
38
|
-
*/
|
|
39
|
-
end(): {
|
|
40
|
-
edits: HashlineEdit[];
|
|
41
|
-
warnings: string[];
|
|
42
|
-
};
|
|
43
|
-
/** Reset to a fresh state so the same instance can drive another parse. */
|
|
44
|
-
reset(): void;
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Drive a full hashline diff through the tokenizer + executor pipeline and
|
|
48
|
-
* return the resulting edits plus any parse-time warnings. This is the
|
|
49
|
-
* convenience entry point most callers want; reach for {@link
|
|
50
|
-
* HashlineTokenizer}/{@link HashlineExecutor} directly only when you need
|
|
51
|
-
* streaming feeds, cross-section state, or custom token handling.
|
|
52
|
-
*/
|
|
53
|
-
export declare function parseHashline(diff: string): {
|
|
54
|
-
edits: HashlineEdit[];
|
|
55
|
-
warnings: string[];
|
|
56
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core hash utilities shared by hashline edit mode, read/search output,
|
|
3
|
-
* and prompt helpers.
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Decoration prefix that may precede a line number in tool output:
|
|
7
|
-
* `>` (context line in grep), `-` (removed line), `*` (match line).
|
|
8
|
-
* Any combination, in any order, surrounded by optional
|
|
9
|
-
* whitespace. Output formatters emit at most one decoration per line; the
|
|
10
|
-
* parser stays liberal because it accepts whatever the model echoes back.
|
|
11
|
-
*/
|
|
12
|
-
export declare const HL_ANCHOR_DECORATION_RE_RAW = "\\s*[>\\-*]*\\s*";
|
|
13
|
-
/** Capture-group regex source for a decorated bare line-number anchor. */
|
|
14
|
-
export declare const HL_ANCHOR_RE_RAW = "\\s*[>\\-*]*\\s*(\\d+)";
|
|
15
|
-
/** Bare positive line-number Lid (no decorations, no captures, no anchors). */
|
|
16
|
-
export declare const HL_LINE_RE_RAW = "[1-9]\\d*";
|
|
17
|
-
/** Capture-group form of {@link HL_LINE_RE_RAW}. */
|
|
18
|
-
export declare const HL_LINE_CAPTURE_RE_RAW = "([1-9]\\d*)";
|
|
19
|
-
/** Four-hex-character file hash carried by a hashline section header. */
|
|
20
|
-
export declare const HL_FILE_HASH_RE_RAW = "[0-9a-f]{4}";
|
|
21
|
-
/** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
|
|
22
|
-
export declare const HL_FILE_HASH_CAPTURE_RE_RAW = "([0-9a-f]{4})";
|
|
23
|
-
/** Separator between a hashline file path and its file hash. */
|
|
24
|
-
export declare const HL_FILE_HASH_SEP = "#";
|
|
25
|
-
/** Separator between a line number and displayed line content in hashline mode. */
|
|
26
|
-
export declare const HL_LINE_BODY_SEP = ":";
|
|
27
|
-
/** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
|
|
28
|
-
export declare const HL_LINE_BODY_SEP_RE_RAW: string;
|
|
29
|
-
/**
|
|
30
|
-
* Representative file hashes for use in user-facing error messages and prompt
|
|
31
|
-
* examples.
|
|
32
|
-
*/
|
|
33
|
-
export declare const HL_FILE_HASH_EXAMPLES: readonly ["1a2b", "3c4d", "9f3e"];
|
|
34
|
-
/**
|
|
35
|
-
* Format a comma-separated list of example anchors with an optional line-number
|
|
36
|
-
* prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
|
|
37
|
-
*/
|
|
38
|
-
export declare function describeAnchorExamples(linePrefix?: string): string;
|
|
39
|
-
/**
|
|
40
|
-
* Substitute every grammar placeholder with the value derived from its
|
|
41
|
-
* TypeScript counterpart. Grammars that don't reference these placeholders
|
|
42
|
-
* pass through unchanged.
|
|
43
|
-
*/
|
|
44
|
-
export declare function resolveHashlineGrammarPlaceholders(grammar: string): string;
|
|
45
|
-
/**
|
|
46
|
-
* op lines have an `ANCHOR<SIGIL>[INLINE_PAYLOAD]` shape, where SIGIL is one of
|
|
47
|
-
* {@link HL_OP_INSERT_BEFORE}, {@link HL_OP_INSERT_AFTER}, {@link HL_OP_REPLACE},
|
|
48
|
-
* or {@link HL_OP_DELETE}. Multi-line payloads follow on subsequent lines
|
|
49
|
-
* prefixed with {@link HL_PAYLOAD_PREFIX}; that prefix is stripped before the
|
|
50
|
-
* payload is written.
|
|
51
|
-
*
|
|
52
|
-
* These constants are the single source of truth for the edit parser, grammar,
|
|
53
|
-
* renderer, and prompt.
|
|
54
|
-
*/
|
|
55
|
-
export declare const HL_OP_INSERT_BEFORE = "\u2191";
|
|
56
|
-
export declare const HL_OP_INSERT_AFTER = "\u2193";
|
|
57
|
-
export declare const HL_OP_REPLACE = ":";
|
|
58
|
-
export declare const HL_OP_DELETE = "!";
|
|
59
|
-
/** Prefix for payload continuation lines. The prefix itself is not written. */
|
|
60
|
-
export declare const HL_PAYLOAD_PREFIX = "+";
|
|
61
|
-
/** All hashline edit op sigils, concatenated for fast membership tests. */
|
|
62
|
-
export declare const HL_OP_CHARS = "\u2191\u2193:!";
|
|
63
|
-
/** Hashline edit file section header marker. */
|
|
64
|
-
export declare const HL_FILE_PREFIX = "\u00B6";
|
|
65
|
-
/**
|
|
66
|
-
* Compute the 4-hex-character hash carried by a hashline section header.
|
|
67
|
-
* The hash normalizes CR characters and trailing whitespace before hashing so
|
|
68
|
-
* platform line endings and display-trimmed lines do not invalidate anchors.
|
|
69
|
-
*/
|
|
70
|
-
export declare function computeFileHash(text: string): string;
|
|
71
|
-
/** Format a hashline section header for a file path and file hash. */
|
|
72
|
-
export declare function formatHashlineHeader(filePath: string, fileHash: string): string;
|
|
73
|
-
/** Formats a single numbered line as `LINE:TEXT`. */
|
|
74
|
-
export declare function formatNumberedLine(lineNumber: number, line: string): string;
|
|
75
|
-
/** Format file text with hashline-mode line-number prefixes for display. */
|
|
76
|
-
export declare function formatNumberedLines(text: string, startLine?: number): string;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export * from "./anchors";
|
|
2
|
-
export * from "./apply";
|
|
3
|
-
export * from "./constants";
|
|
4
|
-
export * from "./diff";
|
|
5
|
-
export * from "./diff-preview";
|
|
6
|
-
export * from "./execute";
|
|
7
|
-
export * from "./executor";
|
|
8
|
-
export * from "./hash";
|
|
9
|
-
export * from "./input";
|
|
10
|
-
export * from "./prefixes";
|
|
11
|
-
export * from "./recovery";
|
|
12
|
-
export * from "./stream";
|
|
13
|
-
export * from "./tokenizer";
|
|
14
|
-
export * from "./types";
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { HashlineInputSection, SplitHashlineOptions } from "./types";
|
|
2
|
-
export declare function containsRecognizableHashlineOperations(input: string): boolean;
|
|
3
|
-
export declare function splitHashlineInput(input: string, options?: SplitHashlineOptions): HashlineInputSection;
|
|
4
|
-
export declare function splitHashlineInputs(input: string, options?: SplitHashlineOptions): HashlineInputSection[];
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare function stripNewLinePrefixes(lines: string[]): string[];
|
|
2
|
-
export declare function stripHashlinePrefixes(lines: string[]): string[];
|
|
3
|
-
/**
|
|
4
|
-
* Normalize line payloads by stripping read/search line prefixes. `null` /
|
|
5
|
-
* `undefined` yield `[]`; a single multiline string is split on `\n`.
|
|
6
|
-
*/
|
|
7
|
-
export declare function hashlineParseText(edit: string[] | string | null | undefined): string[];
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { FileReadCache } from "../edit/file-read-cache";
|
|
2
|
-
import type { HashlineApplyOptions, HashlineEdit } from "./types";
|
|
3
|
-
export interface HashlineRecoveryArgs {
|
|
4
|
-
cache: FileReadCache;
|
|
5
|
-
absolutePath: string;
|
|
6
|
-
currentText: string;
|
|
7
|
-
fileHash: string;
|
|
8
|
-
edits: HashlineEdit[];
|
|
9
|
-
options: HashlineApplyOptions;
|
|
10
|
-
}
|
|
11
|
-
export interface HashlineRecoveryResult {
|
|
12
|
-
lines: string;
|
|
13
|
-
firstChangedLine: number | undefined;
|
|
14
|
-
warnings: string[];
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Attempt to recover from a section file-hash mismatch by replaying the edits
|
|
18
|
-
* against a cached pre-edit snapshot of the file and 3-way-merging the result
|
|
19
|
-
* onto the current on-disk content. Returns `null` when no recovery is possible.
|
|
20
|
-
*/
|
|
21
|
-
export declare function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): HashlineRecoveryResult | null;
|
|
@@ -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
|
-
}
|
package/src/hashline/anchors.ts
DELETED
|
@@ -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
|
-
}
|