@oh-my-pi/pi-coding-agent 15.5.3 → 15.5.6
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 +55 -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 +35 -0
- package/dist/types/edit/hashline/execute.d.ts +28 -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 +11 -0
- package/dist/types/edit/index.d.ts +4 -3
- package/dist/types/edit/normalize.d.ts +4 -16
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +23 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/find.d.ts +7 -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/scripts/build-binary.ts +11 -0
- package/src/config/settings-schema.ts +32 -0
- package/src/config.ts +42 -15
- package/src/edit/diff.ts +5 -3
- package/src/edit/file-snapshot-store.ts +22 -0
- package/src/edit/hashline/diff.ts +95 -0
- package/src/edit/hashline/execute.ts +181 -0
- package/src/edit/hashline/filesystem.ts +129 -0
- package/src/edit/hashline/index.ts +4 -0
- package/src/edit/hashline/params.ts +18 -0
- package/src/edit/index.ts +16 -27
- package/src/edit/normalize.ts +11 -41
- package/src/edit/renderer.ts +15 -8
- package/src/edit/streaming.ts +20 -134
- package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -3
- package/src/index.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/main.ts +2 -1
- package/src/modes/rpc/rpc-client.ts +3 -1
- package/src/prompts/tools/find.md +3 -2
- package/src/sdk.ts +8 -1
- package/src/session/agent-session.ts +18 -2
- package/src/tools/ast-edit.ts +1 -1
- package/src/tools/ast-grep.ts +3 -3
- package/src/tools/fetch.ts +93 -50
- package/src/tools/find.ts +38 -6
- package/src/tools/index.ts +6 -5
- package/src/tools/path-utils.ts +81 -0
- package/src/tools/read.ts +71 -75
- 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 -48
- 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 -60
- 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 -347
- package/src/hashline/grammar.lark +0 -22
- 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 -83
package/src/tools/search.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { computeFileHash, formatHashlineHeader } from "@oh-my-pi/hashline";
|
|
4
5
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
6
|
import { type GrepMatch, GrepOutputMode, type GrepResult, grep } from "@oh-my-pi/pi-natives";
|
|
6
7
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
7
8
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
8
9
|
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import * as z from "zod/v4";
|
|
10
|
-
import {
|
|
11
|
+
import { getFileSnapshotStore } from "../edit/file-snapshot-store";
|
|
11
12
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
12
|
-
import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
|
|
13
13
|
import type { Theme } from "../modes/theme/theme";
|
|
14
14
|
import searchDescription from "../prompts/tools/search.md" with { type: "text" };
|
|
15
15
|
import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead } from "../session/streaming-output";
|
|
@@ -26,7 +26,15 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
|
26
26
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
27
27
|
import { formatMatchLine } from "./match-line-format";
|
|
28
28
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
hasGlobPathChars,
|
|
31
|
+
isLineInRanges,
|
|
32
|
+
type LineRange,
|
|
33
|
+
parseLineRanges,
|
|
34
|
+
resolveReadPath,
|
|
35
|
+
resolveToolSearchScope,
|
|
36
|
+
splitPathAndSel,
|
|
37
|
+
} from "./path-utils";
|
|
30
38
|
import {
|
|
31
39
|
createCachedComponent,
|
|
32
40
|
formatCodeFrameLine,
|
|
@@ -39,13 +47,19 @@ import {
|
|
|
39
47
|
import { ToolError } from "./tool-errors";
|
|
40
48
|
import { toolResult } from "./tool-result";
|
|
41
49
|
|
|
42
|
-
const searchPathEntrySchema = z
|
|
50
|
+
const searchPathEntrySchema = z
|
|
51
|
+
.string()
|
|
52
|
+
.describe(
|
|
53
|
+
'file, directory, glob, internal URL, or "<file>:<lines>" selector (e.g. "src/foo.ts:50-100", "src/foo.ts:50+10", "src/foo.ts:50-100,200-300")',
|
|
54
|
+
);
|
|
43
55
|
const searchSchema = z
|
|
44
56
|
.object({
|
|
45
57
|
pattern: z.string().describe("regex pattern"),
|
|
46
58
|
paths: z
|
|
47
59
|
.union([searchPathEntrySchema, z.array(searchPathEntrySchema).min(1)])
|
|
48
|
-
.describe(
|
|
60
|
+
.describe(
|
|
61
|
+
"file, directory, glob, internal URL, or array of those to search; append `:<lines>` to scope a file to specific line ranges",
|
|
62
|
+
),
|
|
49
63
|
i: z.boolean().optional().describe("case-insensitive search"),
|
|
50
64
|
gitignore: z.boolean().optional().describe("respect gitignore"),
|
|
51
65
|
skip: z
|
|
@@ -98,6 +112,61 @@ function containsTopLevelComma(entry: string): boolean {
|
|
|
98
112
|
return false;
|
|
99
113
|
}
|
|
100
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Parsed `paths` entry — a path (possibly archive-shaped) plus an optional
|
|
117
|
+
* line-range selector peeled off the trailing `:N-M` (or `:N+K`, `:N,M`, …)
|
|
118
|
+
* chunk via {@link splitPathAndSel}.
|
|
119
|
+
*/
|
|
120
|
+
interface SearchPathSpec {
|
|
121
|
+
original: string;
|
|
122
|
+
clean: string;
|
|
123
|
+
ranges?: [LineRange, ...LineRange[]];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parsePathSpecs(rawEntries: readonly string[]): SearchPathSpec[] {
|
|
127
|
+
const specs: SearchPathSpec[] = [];
|
|
128
|
+
for (const entry of rawEntries) {
|
|
129
|
+
const split = splitPathAndSel(entry);
|
|
130
|
+
let clean = entry;
|
|
131
|
+
let ranges: [LineRange, ...LineRange[]] | undefined;
|
|
132
|
+
if (split.sel) {
|
|
133
|
+
const parsed = parseLineRanges(split.sel);
|
|
134
|
+
if (!parsed) {
|
|
135
|
+
throw new ToolError(
|
|
136
|
+
`paths entry "${entry}" — only line-range selectors like ":50-100" are supported (no ":raw"/":conflicts")`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (hasGlobPathChars(split.path)) {
|
|
140
|
+
throw new ToolError(`Line-range selector requires a single file, not a glob: ${entry}`);
|
|
141
|
+
}
|
|
142
|
+
clean = split.path;
|
|
143
|
+
ranges = parsed;
|
|
144
|
+
}
|
|
145
|
+
if (containsTopLevelComma(clean)) {
|
|
146
|
+
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
147
|
+
}
|
|
148
|
+
specs.push({ original: entry, clean, ranges });
|
|
149
|
+
}
|
|
150
|
+
return specs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function mergeRangesInto(map: Map<string, LineRange[]>, absKey: string, ranges: readonly LineRange[]): void {
|
|
154
|
+
// Concat-without-merge is correct: `isLineInRanges` scans linearly, so
|
|
155
|
+
// duplicates/overlaps only cost a few extra comparisons per match.
|
|
156
|
+
const existing = map.get(absKey);
|
|
157
|
+
if (existing) {
|
|
158
|
+
existing.push(...ranges);
|
|
159
|
+
} else {
|
|
160
|
+
map.set(absKey, [...ranges]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function matchAbsolutePath(matchPath: string, searchPath: string): string {
|
|
165
|
+
if (matchPath === "") return searchPath;
|
|
166
|
+
if (path.isAbsolute(matchPath)) return matchPath;
|
|
167
|
+
return path.resolve(searchPath, matchPath);
|
|
168
|
+
}
|
|
169
|
+
|
|
101
170
|
/**
|
|
102
171
|
* Pre-resolve any `paths` entries that point at a member inside an archive
|
|
103
172
|
* (e.g. `bundle.zip:src/foo.ts`, `release.tar.gz:notes.md`). Native grep
|
|
@@ -253,12 +322,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
253
322
|
if (normalizedSkip < 0 || !Number.isFinite(normalizedSkip)) {
|
|
254
323
|
throw new ToolError("Skip must be a non-negative number");
|
|
255
324
|
}
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
throw new ToolError('paths is an array — pass ["a", "b"] not ["a,b"]');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
325
|
+
const rawEntries = toPathList(rawPaths);
|
|
326
|
+
const pathSpecs = parsePathSpecs(rawEntries);
|
|
327
|
+
const paths = pathSpecs.map(spec => spec.clean);
|
|
262
328
|
const {
|
|
263
329
|
resolvedPaths,
|
|
264
330
|
displayMap: archiveDisplayMap,
|
|
@@ -267,6 +333,33 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
267
333
|
cleanup: cleanupArchiveScratch,
|
|
268
334
|
} = await resolveArchiveSearchPaths(paths, this.session.cwd);
|
|
269
335
|
try {
|
|
336
|
+
// Build the per-file line-range filter (keyed by absolute path) now that
|
|
337
|
+
// archive entries have been materialized to scratch files. Plain entries
|
|
338
|
+
// resolve through `resolveReadPath`; archive entries are keyed by the
|
|
339
|
+
// scratch path that grep will actually report against.
|
|
340
|
+
const rangesByAbsPath = new Map<string, LineRange[]>();
|
|
341
|
+
for (let idx = 0; idx < pathSpecs.length; idx++) {
|
|
342
|
+
const spec = pathSpecs[idx];
|
|
343
|
+
if (!spec.ranges) continue;
|
|
344
|
+
const resolved = resolvedPaths[idx];
|
|
345
|
+
if (resolved === spec.clean && !archiveDisplayMap.has(resolved)) {
|
|
346
|
+
// Non-archive entry; ensure the cleaned path resolves to a regular file.
|
|
347
|
+
const absKey = path.resolve(resolveReadPath(spec.clean, this.session.cwd));
|
|
348
|
+
const stats = await stat(absKey).catch(() => null);
|
|
349
|
+
if (!stats) {
|
|
350
|
+
throw new ToolError(`Path not found for line-range selector: ${spec.original}`);
|
|
351
|
+
}
|
|
352
|
+
if (!stats.isFile()) {
|
|
353
|
+
throw new ToolError(`Line-range selector requires a single file: ${spec.original} is a directory`);
|
|
354
|
+
}
|
|
355
|
+
mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
|
|
356
|
+
} else {
|
|
357
|
+
// Archive entry — `resolveArchiveSearchPaths` substituted a scratch path.
|
|
358
|
+
const absKey = path.resolve(resolved);
|
|
359
|
+
mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
270
363
|
if (archiveUnreadable.length > 0 && resolvedPaths.length === archiveUnreadable.length) {
|
|
271
364
|
// All inputs were archive selectors we couldn't materialize; surface the
|
|
272
365
|
// reason instead of a downstream "path not found" from the scope resolver.
|
|
@@ -390,12 +483,38 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
390
483
|
}
|
|
391
484
|
throw err;
|
|
392
485
|
}
|
|
486
|
+
if (rangesByAbsPath.size > 0) {
|
|
487
|
+
const filteredMatches: GrepMatch[] = [];
|
|
488
|
+
for (const match of result.matches) {
|
|
489
|
+
const abs = matchAbsolutePath(match.path, searchPath);
|
|
490
|
+
const ranges = rangesByAbsPath.get(abs);
|
|
491
|
+
if (!ranges) {
|
|
492
|
+
// Path has no line-range constraint (e.g. a peer entry without `:N-M`).
|
|
493
|
+
filteredMatches.push(match);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (!isLineInRanges(match.lineNumber, ranges)) continue;
|
|
497
|
+
// Drop context lines that fall outside the allowed ranges; they would
|
|
498
|
+
// otherwise leak content the caller explicitly excluded.
|
|
499
|
+
const trimBefore = match.contextBefore?.filter(c => isLineInRanges(c.lineNumber, ranges));
|
|
500
|
+
const trimAfter = match.contextAfter?.filter(c => isLineInRanges(c.lineNumber, ranges));
|
|
501
|
+
filteredMatches.push({
|
|
502
|
+
...match,
|
|
503
|
+
contextBefore: trimBefore && trimBefore.length > 0 ? trimBefore : undefined,
|
|
504
|
+
contextAfter: trimAfter && trimAfter.length > 0 ? trimAfter : undefined,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
result = {
|
|
508
|
+
matches: filteredMatches,
|
|
509
|
+
totalMatches: filteredMatches.length,
|
|
510
|
+
filesWithMatches: new Set(filteredMatches.map(match => match.path)).size,
|
|
511
|
+
filesSearched: result.filesSearched,
|
|
512
|
+
limitReached: result.limitReached,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
393
515
|
if (archiveDisplayMap.size > 0) {
|
|
394
516
|
for (const match of result.matches) {
|
|
395
|
-
|
|
396
|
-
if (match.path === "") abs = searchPath;
|
|
397
|
-
else if (path.isAbsolute(match.path)) abs = match.path;
|
|
398
|
-
else abs = path.resolve(searchPath, match.path);
|
|
517
|
+
const abs = matchAbsolutePath(match.path, searchPath);
|
|
399
518
|
const display = archiveDisplayMap.get(abs);
|
|
400
519
|
if (display) match.path = display;
|
|
401
520
|
}
|
|
@@ -552,7 +671,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
552
671
|
fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
|
|
553
672
|
}
|
|
554
673
|
if (cacheEntries.length > 0 && hashContext) {
|
|
555
|
-
|
|
674
|
+
getFileSnapshotStore(this.session).recordSparse(hashContext.absolutePath, cacheEntries, {
|
|
556
675
|
fileHash: hashContext.fileHash,
|
|
557
676
|
});
|
|
558
677
|
}
|
package/src/tools/write.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { stripHashlinePrefixes } from "@oh-my-pi/hashline";
|
|
4
5
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
8
|
import { isEnoent, isRecord, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import * as z from "zod/v4";
|
|
9
|
-
import { stripHashlinePrefixes } from "../edit";
|
|
10
10
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
11
|
import { InternalUrlRouter } from "../internal-urls";
|
|
12
12
|
import { parseInternalUrl } from "../internal-urls/parse";
|
|
@@ -487,7 +487,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
487
487
|
const batchRequest = getLspBatchRequest(context?.toolCall);
|
|
488
488
|
const diagnostics = await this.#writethrough(absolutePath, newContent, signal, undefined, batchRequest);
|
|
489
489
|
invalidateFsScanAfterWrite(absolutePath);
|
|
490
|
-
this.session.
|
|
490
|
+
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
491
491
|
this.session.conflictHistory?.invalidate(entry.id);
|
|
492
492
|
|
|
493
493
|
const range =
|
|
@@ -609,7 +609,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
609
609
|
|
|
610
610
|
const diagnostics = await this.#writethrough(absolutePath, text, signal, undefined, batchRequest);
|
|
611
611
|
invalidateFsScanAfterWrite(absolutePath);
|
|
612
|
-
this.session.
|
|
612
|
+
this.session.fileSnapshotStore?.invalidate(absolutePath);
|
|
613
613
|
for (const entry of fileEntries) history.invalidate(entry.id);
|
|
614
614
|
succeededFiles.push({ displayPath: sample.displayPath, count: fileEntries.length });
|
|
615
615
|
totalResolvedIds += fileEntries.length;
|
package/src/utils/changelog.ts
CHANGED
|
@@ -8,10 +8,18 @@ export interface ChangelogEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Parse changelog entries from
|
|
12
|
-
*
|
|
11
|
+
* Parse changelog entries from the file at `changelogPath`. Scans for `## [x.y.z]`
|
|
12
|
+
* headings and collects each block until the next heading or EOF.
|
|
13
|
+
*
|
|
14
|
+
* Returns `[]` when `changelogPath` is `undefined` (package directory not
|
|
15
|
+
* resolvable — see `getChangelogPath`) or the file is missing. Callers MUST NOT
|
|
16
|
+
* synthesize a fallback path from the host project's cwd; doing so caused issue
|
|
17
|
+
* #1423 (the host project's `CHANGELOG.md` was rendered as omp's).
|
|
13
18
|
*/
|
|
14
|
-
export async function parseChangelog(changelogPath: string): Promise<ChangelogEntry[]> {
|
|
19
|
+
export async function parseChangelog(changelogPath: string | undefined): Promise<ChangelogEntry[]> {
|
|
20
|
+
if (!changelogPath) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
15
23
|
try {
|
|
16
24
|
const content = await Bun.file(changelogPath).text();
|
|
17
25
|
const lines = content.split("\n");
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import { computeFileHash, formatHashlineHeader, formatNumberedLines } from "@oh-my-pi/hashline";
|
|
10
11
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
11
12
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
12
13
|
import { glob } from "@oh-my-pi/pi-natives";
|
|
13
14
|
import { fuzzyMatch } from "@oh-my-pi/pi-tui";
|
|
14
15
|
import { formatAge, formatBytes, readImageMetadata } from "@oh-my-pi/pi-utils";
|
|
15
|
-
import { computeFileHash, formatHashlineHeader, formatNumberedLines } from "../hashline/hash";
|
|
16
16
|
import type { FileMentionMessage } from "../session/messages";
|
|
17
17
|
import {
|
|
18
18
|
DEFAULT_MAX_BYTES,
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ToolSession } from "../tools";
|
|
2
|
-
export interface FileReadSnapshot {
|
|
3
|
-
/** 1-indexed line number → exact line content as observed by `read`/`search`. */
|
|
4
|
-
lines: Map<number, string>;
|
|
5
|
-
/** Full normalized text when the read path observed the whole file. */
|
|
6
|
-
fullText?: string;
|
|
7
|
-
/** 4-hex hash of `fullText`, or a sparse snapshot hash supplied by search. */
|
|
8
|
-
fileHash?: string;
|
|
9
|
-
recordedAt: number;
|
|
10
|
-
}
|
|
11
|
-
interface FileReadSnapshotMetadata {
|
|
12
|
-
fullText?: string;
|
|
13
|
-
fileHash?: string;
|
|
14
|
-
}
|
|
15
|
-
export declare class FileReadCache {
|
|
16
|
-
#private;
|
|
17
|
-
/** Look up the most recent snapshot for `absPath`, or `null` if absent. */
|
|
18
|
-
get(absPath: string): FileReadSnapshot | null;
|
|
19
|
-
/** Look up the most recent snapshot for `absPath` whose file hash matches. */
|
|
20
|
-
getByHash(absPath: string, fileHash: string): FileReadSnapshot | null;
|
|
21
|
-
/** Record a contiguous run of lines (e.g. from a `read` tool). `startLine` is 1-indexed. */
|
|
22
|
-
recordContiguous(absPath: string, startLine: number, lines: readonly string[], metadata?: FileReadSnapshotMetadata): void;
|
|
23
|
-
/** Record sparse `(lineNumber, content)` pairs (e.g. `search` matches plus context). */
|
|
24
|
-
recordSparse(absPath: string, entries: Iterable<readonly [number, string]>, metadata?: FileReadSnapshotMetadata): void;
|
|
25
|
-
/** Drop the snapshot history for a single path. */
|
|
26
|
-
invalidate(absPath: string): void;
|
|
27
|
-
/** Drop every snapshot history. */
|
|
28
|
-
clear(): void;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Look up (or lazily create) the file-read cache attached to a session. The
|
|
32
|
-
* cache is stored as `session.fileReadCache` so it lives exactly as long as
|
|
33
|
-
* the session itself.
|
|
34
|
-
*/
|
|
35
|
-
export declare function getFileReadCache(session: ToolSession): FileReadCache;
|
|
36
|
-
export {};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
export declare function formatFullAnchorRequirement(raw?: string): string;
|
|
2
|
-
export declare function parseTag(ref: string): {
|
|
3
|
-
line: number;
|
|
4
|
-
};
|
|
5
|
-
export interface HashlineMismatchDetails {
|
|
6
|
-
path?: string;
|
|
7
|
-
expectedFileHash: string;
|
|
8
|
-
actualFileHash: string;
|
|
9
|
-
fileLines: string[];
|
|
10
|
-
anchorLines?: readonly number[];
|
|
11
|
-
}
|
|
12
|
-
export declare class HashlineMismatchError extends Error {
|
|
13
|
-
readonly path: string | undefined;
|
|
14
|
-
readonly expectedFileHash: string;
|
|
15
|
-
readonly actualFileHash: string;
|
|
16
|
-
readonly fileLines: string[];
|
|
17
|
-
readonly anchorLines: readonly number[];
|
|
18
|
-
constructor(details: HashlineMismatchDetails);
|
|
19
|
-
get displayMessage(): string;
|
|
20
|
-
static rejectionHeader(details: HashlineMismatchDetails): string[];
|
|
21
|
-
static formatDisplayMessage(details: HashlineMismatchDetails): string;
|
|
22
|
-
static formatMessage(details: HashlineMismatchDetails): string;
|
|
23
|
-
}
|
|
24
|
-
export declare function validateLineRef(ref: {
|
|
25
|
-
line: number;
|
|
26
|
-
}, fileLines: string[]): void;
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { HashlineApplyOptions, HashlineEdit } from "./types";
|
|
2
|
-
export interface HashlineApplyResult {
|
|
3
|
-
lines: string;
|
|
4
|
-
firstChangedLine?: number;
|
|
5
|
-
warnings?: string[];
|
|
6
|
-
noopEdits?: HashlineNoopEdit[];
|
|
7
|
-
}
|
|
8
|
-
export interface HashlineNoopEdit {
|
|
9
|
-
editIndex: number;
|
|
10
|
-
loc: string;
|
|
11
|
-
reason: string;
|
|
12
|
-
current: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function applyHashlineEdits(text: string, edits: HashlineEdit[], options?: HashlineApplyOptions): HashlineApplyResult;
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/** Lines of context shown either side of a hash mismatch. */
|
|
2
|
-
export declare const MISMATCH_CONTEXT = 2;
|
|
3
|
-
/** Optional patch envelope start marker; silently consumed when present. */
|
|
4
|
-
export declare const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
5
|
-
/** Optional patch envelope end marker; terminates parsing when encountered. */
|
|
6
|
-
export declare const END_PATCH_MARKER = "*** End Patch";
|
|
7
|
-
/**
|
|
8
|
-
* Recovery sentinel emitted by the agent loop when a contaminated
|
|
9
|
-
* `to=functions.edit` stream is truncated mid-call (see
|
|
10
|
-
* `docs/ERRATA-GPT5-HARMONY.md`). Behaves like `END_PATCH_MARKER` for
|
|
11
|
-
* parsing — terminates the line loop — and additionally surfaces a
|
|
12
|
-
* warning in the tool result so the model knows to re-issue any
|
|
13
|
-
* remaining edits.
|
|
14
|
-
*/
|
|
15
|
-
export declare const ABORT_MARKER = "*** Abort";
|
|
16
|
-
/** Warning text appended to the tool result when ABORT_MARKER terminates parsing. */
|
|
17
|
-
export declare const ABORT_WARNING = "Tool stream truncated mid-call due to detected output corruption. Applied ops above are valid. Re-issue any remaining edits.";
|
|
18
|
-
/**
|
|
19
|
-
* Warning text appended when two consecutive `A-B:` ops on the exact same
|
|
20
|
-
* range get coalesced (model painted a before/after pair). The second op
|
|
21
|
-
* wins; the first op's payload is silently discarded.
|
|
22
|
-
*/
|
|
23
|
-
export declare const REPLACE_PAIR_COALESCED_WARNING = "Detected an identical-range before/after replace pair; kept only the second block's payload. Issue ONE op per range \u2014 the payload is the final desired content, never both old and new.";
|
|
24
|
-
/**
|
|
25
|
-
* Warning text appended when un-prefixed continuation lines are accepted as
|
|
26
|
-
* implicit payload (lenient legacy behavior). The model authored a multi-line
|
|
27
|
-
* replace without `+` prefixes; the parser accepted it because the lines did
|
|
28
|
-
* not classify as ops/headers/payloads, but the canonical syntax requires `+`
|
|
29
|
-
* on every continuation line after the op.
|
|
30
|
-
*/
|
|
31
|
-
export declare const IMPLICIT_CONTINUATION_WARNING = "Accepted continuation line(s) without the `+` prefix as implicit payload. Canonical syntax is `A-B:` followed by `+` on every continuation row; without `+`, lines that look like ops will be parsed as new ops instead of payload. Prefer the explicit form.";
|
|
32
|
-
/**
|
|
33
|
-
* Warning text appended when an inner `LINE:TEXT` (or sub-range `A-B:TEXT`)
|
|
34
|
-
* op arrives while an outer `A-B:` replace is still pending and the inner
|
|
35
|
-
* anchor falls inside the outer range. The model used the read-output
|
|
36
|
-
* `LINE:TEXT` format as if it were a payload-continuation line; we strip the
|
|
37
|
-
* `LINE:` prefix and append the body to the pending payload, but warn so the
|
|
38
|
-
* canonical `+`-continuation form remains preferred.
|
|
39
|
-
*/
|
|
40
|
-
export declare const PAYLOAD_LINE_PREFIX_DEMOTED_WARNING = "Detected one or more `LINE:TEXT` lines whose anchors fell inside a pending replace range; treated them as payload-continuation lines and stripped the `LINE:` prefix. Inside an `A-B:` block, every payload line must be on its own row prefixed with `+` \u2014 never reuse the read-output gutter format.";
|
|
41
|
-
/**
|
|
42
|
-
* Warning text appended when an op carries an inline payload (`LINE:TEXT`,
|
|
43
|
-
* `A-B:TEXT`, `LINE↑TEXT`, `LINE↓TEXT`). Canonical syntax is bare op +
|
|
44
|
-
* `+`-prefixed continuation rows; we accept the inline form leniently so the
|
|
45
|
-
* model's first-attempt edit still lands, but warn so the canonical form
|
|
46
|
-
* remains preferred.
|
|
47
|
-
*/
|
|
48
|
-
export declare const INLINE_PAYLOAD_ACCEPTED_WARNING = "Accepted inline payload on the op line (e.g. `LINE:CONTENT`, `LINE\u2191CONTENT`). Canonical syntax is the bare op followed by `+`-prefixed payload rows on the next line(s). Prefer the explicit form.";
|
|
@@ -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;
|