@oh-my-pi/pi-coding-agent 14.9.1 → 14.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +38 -0
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/parse.ts +156 -255
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +38 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +209 -15
- package/src/extensibility/extensions/runner.ts +173 -177
- package/src/hashline/apply.ts +8 -24
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/execute.ts +0 -1
- package/src/hashline/grammar.lark +16 -27
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -40
- package/src/hashline/types.ts +1 -2
- package/src/internal-urls/agent-protocol.ts +1 -0
- package/src/internal-urls/artifact-protocol.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/internal-urls/jobs-protocol.ts +1 -0
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/internal-urls/mcp-protocol.ts +1 -0
- package/src/internal-urls/memory-protocol.ts +1 -0
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +2 -1
- package/src/internal-urls/rule-protocol.ts +1 -0
- package/src/internal-urls/skill-protocol.ts +1 -0
- package/src/internal-urls/types.ts +18 -2
- package/src/mcp/transports/http.ts +49 -47
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/now-prompt.md +7 -0
- package/src/prompts/system/project-prompt.md +2 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +154 -233
- package/src/prompts/tools/bash.md +0 -24
- package/src/prompts/tools/eval.md +26 -13
- package/src/prompts/tools/hashline.md +1 -4
- package/src/sdk.ts +12 -22
- package/src/session/agent-session.ts +49 -17
- package/src/system-prompt.ts +38 -104
- package/src/task/executor.ts +15 -9
- package/src/task/index.ts +38 -33
- package/src/task/render.ts +4 -2
- package/src/tools/bash.ts +15 -41
- package/src/tools/eval.ts +13 -36
- package/src/tools/index.ts +0 -3
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +71 -49
- package/src/tools/search.ts +13 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/workspace-tree.ts +210 -410
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
package/src/tools/read.ts
CHANGED
|
@@ -73,20 +73,6 @@ const PROSE_SUMMARY_EXTENSIONS = new Set([".md", ".txt"]);
|
|
|
73
73
|
// Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
|
|
74
74
|
const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
|
|
75
75
|
|
|
76
|
-
const READ_DIRECTORY_EXCLUDED_DIRS = new Set([
|
|
77
|
-
"node_modules",
|
|
78
|
-
".git",
|
|
79
|
-
".next",
|
|
80
|
-
"dist",
|
|
81
|
-
"build",
|
|
82
|
-
"target",
|
|
83
|
-
".venv",
|
|
84
|
-
".cache",
|
|
85
|
-
".turbo",
|
|
86
|
-
".parcel-cache",
|
|
87
|
-
"coverage",
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
76
|
function isRemoteMountPath(absolutePath: string): boolean {
|
|
91
77
|
return absolutePath.startsWith(REMOTE_MOUNT_PREFIX);
|
|
92
78
|
}
|
|
@@ -476,34 +462,67 @@ type ReadParams = ReadToolInput;
|
|
|
476
462
|
type ParsedSelector =
|
|
477
463
|
| { kind: "none" }
|
|
478
464
|
| { kind: "raw" }
|
|
479
|
-
| { kind: "lines"; startLine: number; endLine: number | undefined };
|
|
465
|
+
| { kind: "lines"; startLine: number; endLine: number | undefined; raw?: boolean };
|
|
480
466
|
|
|
481
467
|
const LINE_RANGE_RE = /^L?(\d+)(?:([-+])L?(\d+))?$/i;
|
|
482
468
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
469
|
+
/** Returns true when the selector requested verbatim/raw output (alone or combined with a range). */
|
|
470
|
+
function isRawSelector(parsed: ParsedSelector): boolean {
|
|
471
|
+
return parsed.kind === "raw" || (parsed.kind === "lines" && parsed.raw === true);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function parseLineRangeChunk(sel: string): { startLine: number; endLine: number | undefined } | null {
|
|
486
475
|
const lineMatch = LINE_RANGE_RE.exec(sel);
|
|
487
|
-
if (lineMatch)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
476
|
+
if (!lineMatch) return null;
|
|
477
|
+
const rawStart = Number.parseInt(lineMatch[1]!, 10);
|
|
478
|
+
if (rawStart < 1) {
|
|
479
|
+
throw new ToolError("Line selector 0 is invalid; lines are 1-indexed. Use :1.");
|
|
480
|
+
}
|
|
481
|
+
const sep = lineMatch[2];
|
|
482
|
+
const rhs = lineMatch[3] ? Number.parseInt(lineMatch[3], 10) : undefined;
|
|
483
|
+
let rawEnd: number | undefined;
|
|
484
|
+
if (sep === "+") {
|
|
485
|
+
if (rhs === undefined || rhs < 1) {
|
|
486
|
+
throw new ToolError(`Invalid range ${rawStart}+${rhs ?? 0}: count must be >= 1.`);
|
|
491
487
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
488
|
+
rawEnd = rawStart + rhs - 1;
|
|
489
|
+
} else if (sep === "-") {
|
|
490
|
+
if (rhs === undefined || rhs < rawStart) {
|
|
491
|
+
throw new ToolError(`Invalid range ${rawStart}-${rhs ?? 0}: end must be >= start.`);
|
|
492
|
+
}
|
|
493
|
+
rawEnd = rhs;
|
|
494
|
+
}
|
|
495
|
+
return { startLine: rawStart, endLine: rawEnd };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function parseSel(sel: string | undefined): ParsedSelector {
|
|
499
|
+
if (!sel || sel.length === 0) return { kind: "none" };
|
|
500
|
+
|
|
501
|
+
// Compound selector: `1-50:raw` or `raw:1-50`. Split into chunks and accept
|
|
502
|
+
// any combination of one line range and the literal `raw`.
|
|
503
|
+
if (sel.includes(":")) {
|
|
504
|
+
const chunks = sel.split(":");
|
|
505
|
+
if (chunks.length === 2) {
|
|
506
|
+
const [a, b] = chunks as [string, string];
|
|
507
|
+
const aIsRaw = a.toLowerCase() === "raw";
|
|
508
|
+
const bIsRaw = b.toLowerCase() === "raw";
|
|
509
|
+
const rangeChunk = aIsRaw ? b : bIsRaw ? a : null;
|
|
510
|
+
const rawChunk = aIsRaw ? a : bIsRaw ? b : null;
|
|
511
|
+
if (rangeChunk !== null && rawChunk !== null) {
|
|
512
|
+
const range = parseLineRangeChunk(rangeChunk);
|
|
513
|
+
if (range) {
|
|
514
|
+
return { kind: "lines", startLine: range.startLine, endLine: range.endLine, raw: true };
|
|
515
|
+
}
|
|
503
516
|
}
|
|
504
|
-
rawEnd = rhs;
|
|
505
517
|
}
|
|
506
|
-
|
|
518
|
+
// Unrecognized compound — fall through (sqlite/archive/url consume their own colon syntax).
|
|
519
|
+
return { kind: "none" };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (sel.toLowerCase() === "raw") return { kind: "raw" };
|
|
523
|
+
const range = parseLineRangeChunk(sel);
|
|
524
|
+
if (range) {
|
|
525
|
+
return { kind: "lines", startLine: range.startLine, endLine: range.endLine };
|
|
507
526
|
}
|
|
508
527
|
// Unrecognized selectors fall through; sqlite/archive/url readers consume their own colon syntax.
|
|
509
528
|
return { kind: "none" };
|
|
@@ -667,9 +686,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
667
686
|
entityLabel: string;
|
|
668
687
|
ignoreResultLimits?: boolean;
|
|
669
688
|
raw?: boolean;
|
|
689
|
+
immutable?: boolean;
|
|
670
690
|
},
|
|
671
691
|
): AgentToolResult<ReadToolDetails> {
|
|
672
|
-
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw });
|
|
692
|
+
const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
|
|
673
693
|
const details = options.details ?? {};
|
|
674
694
|
const allLines = text.split("\n");
|
|
675
695
|
const totalLines = allLines.length;
|
|
@@ -1153,6 +1173,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1153
1173
|
details: { ...cached.details },
|
|
1154
1174
|
sourceUrl: cached.details.finalUrl,
|
|
1155
1175
|
entityLabel: "URL output",
|
|
1176
|
+
immutable: true,
|
|
1156
1177
|
});
|
|
1157
1178
|
}
|
|
1158
1179
|
return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw }, signal);
|
|
@@ -1164,7 +1185,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1164
1185
|
if (internalRouter?.canHandle(internalTarget.path)) {
|
|
1165
1186
|
const parsed = parseSel(internalTarget.sel);
|
|
1166
1187
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1167
|
-
return this.#handleInternalUrl(internalTarget.path, offset, limit);
|
|
1188
|
+
return this.#handleInternalUrl(internalTarget.path, offset, limit, { raw: isRawSelector(parsed) });
|
|
1168
1189
|
}
|
|
1169
1190
|
|
|
1170
1191
|
const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
|
|
@@ -1178,7 +1199,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1178
1199
|
limit,
|
|
1179
1200
|
{ ...archivePath, archiveSubPath: archiveSubPath.path },
|
|
1180
1201
|
signal,
|
|
1181
|
-
{ raw: archiveParsed
|
|
1202
|
+
{ raw: isRawSelector(archiveParsed) },
|
|
1182
1203
|
);
|
|
1183
1204
|
}
|
|
1184
1205
|
|
|
@@ -1307,7 +1328,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1307
1328
|
throw error;
|
|
1308
1329
|
}
|
|
1309
1330
|
}
|
|
1310
|
-
} else if (isNotebookPath(absolutePath) && parsed
|
|
1331
|
+
} else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
|
|
1311
1332
|
const { offset, limit } = selToOffsetLimit(parsed);
|
|
1312
1333
|
return this.#buildInMemoryTextResult(
|
|
1313
1334
|
await readEditableNotebookText(absolutePath, localReadPath),
|
|
@@ -1435,7 +1456,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1435
1456
|
getFileReadCache(this.session).recordContiguous(absolutePath, startLineDisplay, collectedLines);
|
|
1436
1457
|
}
|
|
1437
1458
|
|
|
1438
|
-
const isRawMode = parsed
|
|
1459
|
+
const isRawMode = isRawSelector(parsed);
|
|
1439
1460
|
const shouldAddHashLines = !isRawMode && displayMode.hashLines;
|
|
1440
1461
|
const shouldAddLineNumbers = isRawMode ? false : shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
1441
1462
|
let capturedDisplayContent: { text: string; startLine: number } | undefined;
|
|
@@ -1524,7 +1545,12 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1524
1545
|
* Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://).
|
|
1525
1546
|
* Supports pagination via offset/limit but rejects them when query extraction is used.
|
|
1526
1547
|
*/
|
|
1527
|
-
async #handleInternalUrl(
|
|
1548
|
+
async #handleInternalUrl(
|
|
1549
|
+
url: string,
|
|
1550
|
+
offset?: number,
|
|
1551
|
+
limit?: number,
|
|
1552
|
+
options?: { raw?: boolean },
|
|
1553
|
+
): Promise<AgentToolResult<ReadToolDetails>> {
|
|
1528
1554
|
const internalRouter = this.session.internalRouter!;
|
|
1529
1555
|
|
|
1530
1556
|
// Check if URL has query extraction (agent:// only).
|
|
@@ -1564,6 +1590,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1564
1590
|
sourceInternal: url,
|
|
1565
1591
|
entityLabel: "resource",
|
|
1566
1592
|
ignoreResultLimits: scheme === "skill",
|
|
1593
|
+
immutable: resource.immutable,
|
|
1594
|
+
raw: options?.raw,
|
|
1567
1595
|
});
|
|
1568
1596
|
}
|
|
1569
1597
|
|
|
@@ -1581,15 +1609,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1581
1609
|
try {
|
|
1582
1610
|
tree = await buildDirectoryTree(absolutePath, {
|
|
1583
1611
|
maxDepth: READ_DIRECTORY_MAX_DEPTH,
|
|
1584
|
-
|
|
1585
|
-
|
|
1612
|
+
perDirLimit: READ_DIRECTORY_CHILD_LIMIT,
|
|
1613
|
+
rootLimit: null,
|
|
1586
1614
|
lineCap: limit ?? null,
|
|
1587
|
-
lineCapProtectedDepth: 1,
|
|
1588
|
-
hidden: true,
|
|
1589
|
-
gitignore: false,
|
|
1590
|
-
cache: true,
|
|
1591
|
-
excludedDirectoryNames: READ_DIRECTORY_EXCLUDED_DIRS,
|
|
1592
|
-
rootLabel: ".",
|
|
1593
1615
|
});
|
|
1594
1616
|
} catch (error) {
|
|
1595
1617
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/tools/search.ts
CHANGED
|
@@ -121,7 +121,6 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
121
121
|
const patternHasNewline = normalizedPattern.includes("\n") || normalizedPattern.includes("\\n");
|
|
122
122
|
const effectiveMultiline = patternHasNewline;
|
|
123
123
|
|
|
124
|
-
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
125
124
|
const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
|
|
126
125
|
let searchPath: string;
|
|
127
126
|
let scopePath: string;
|
|
@@ -134,6 +133,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
134
133
|
}
|
|
135
134
|
const internalRouter = this.session.internalRouter;
|
|
136
135
|
const resolvedPathInputs: string[] = [];
|
|
136
|
+
// Absolute filesystem paths whose source is immutable (e.g. artifact://,
|
|
137
|
+
// pi://, skill://). Hashline anchors are suppressed for these on a
|
|
138
|
+
// per-file basis, leaving editable mixed-in files untouched.
|
|
139
|
+
const immutableSourcePaths = new Set<string>();
|
|
137
140
|
for (const rawPath of rawPaths) {
|
|
138
141
|
if (!internalRouter?.canHandle(rawPath)) {
|
|
139
142
|
resolvedPathInputs.push(rawPath);
|
|
@@ -146,8 +149,13 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
146
149
|
if (!resource.sourcePath) {
|
|
147
150
|
throw new ToolError(`Cannot search internal URL without a backing file: ${rawPath}`);
|
|
148
151
|
}
|
|
152
|
+
if (resource.immutable) {
|
|
153
|
+
immutableSourcePaths.add(path.resolve(resource.sourcePath));
|
|
154
|
+
}
|
|
149
155
|
resolvedPathInputs.push(resource.sourcePath);
|
|
150
156
|
}
|
|
157
|
+
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
158
|
+
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
|
151
159
|
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
152
160
|
// directory is gone, and only error if every entry is missing. Single
|
|
153
161
|
// missing path keeps the original ENOENT semantics.
|
|
@@ -336,6 +344,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
336
344
|
const modelOut: string[] = [];
|
|
337
345
|
const displayOut: string[] = [];
|
|
338
346
|
const fileMatches = matchesByFile.get(relativePath) ?? [];
|
|
347
|
+
const absoluteFilePath = path.resolve(this.session.cwd, relativePath);
|
|
348
|
+
const useHashLines = immutableSourcePaths.has(absoluteFilePath)
|
|
349
|
+
? immutableDisplayMode.hashLines
|
|
350
|
+
: baseDisplayMode.hashLines;
|
|
339
351
|
const lineNumberWidth = fileMatches.reduce((width, match) => {
|
|
340
352
|
let nextWidth = Math.max(width, String(match.lineNumber).length);
|
|
341
353
|
for (const ctx of match.contextBefore ?? []) {
|
|
@@ -21,17 +21,23 @@ export interface FileDisplayModeSession {
|
|
|
21
21
|
/**
|
|
22
22
|
* Computes effective line display mode from session settings/env.
|
|
23
23
|
* Hashline mode takes precedence and implies line-addressed output everywhere.
|
|
24
|
-
* Hashlines are suppressed when the edit tool is not available (e.g. explore agents)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Hashlines are suppressed when the edit tool is not available (e.g. explore agents),
|
|
25
|
+
* when the caller signals a `raw` read, and when the source is `immutable`
|
|
26
|
+
* (e.g. internal URLs like artifact://, agent://, memory:// — there is no edit
|
|
27
|
+
* path that could consume the anchors). Raw output is returned as-is.
|
|
27
28
|
*/
|
|
28
|
-
export function resolveFileDisplayMode(
|
|
29
|
+
export function resolveFileDisplayMode(
|
|
30
|
+
session: FileDisplayModeSession,
|
|
31
|
+
options?: { raw?: boolean; immutable?: boolean },
|
|
32
|
+
): FileDisplayMode {
|
|
29
33
|
const { settings } = session;
|
|
30
34
|
const hasEditTool = session.hasEditTool ?? true;
|
|
31
35
|
const editMode = resolveEditMode(session);
|
|
32
36
|
const usesHashLineAnchors = editMode === "hashline";
|
|
33
37
|
const raw = options?.raw === true;
|
|
34
|
-
const
|
|
38
|
+
const immutable = options?.immutable === true;
|
|
39
|
+
const hashLines =
|
|
40
|
+
!raw && !immutable && hasEditTool && usesHashLineAnchors && settings.get("readHashLines") !== false;
|
|
35
41
|
return {
|
|
36
42
|
hashLines,
|
|
37
43
|
lineNumbers: !raw && (hashLines || settings.get("readLineNumbers") === true),
|