@oh-my-pi/pi-coding-agent 13.10.1 → 13.11.1
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 +71 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +3 -1
- package/src/commit/agentic/index.ts +7 -1
- package/src/commit/analysis/conventional.ts +5 -1
- package/src/commit/analysis/summary.ts +5 -1
- package/src/commit/changelog/generate.ts +5 -1
- package/src/commit/changelog/index.ts +4 -0
- package/src/commit/map-reduce/index.ts +5 -0
- package/src/commit/map-reduce/map-phase.ts +17 -2
- package/src/commit/map-reduce/reduce-phase.ts +5 -1
- package/src/commit/model-selection.ts +38 -26
- package/src/commit/pipeline.ts +22 -11
- package/src/config/model-registry.ts +98 -17
- package/src/config/settings-schema.ts +31 -12
- package/src/config.ts +10 -3
- package/src/discovery/helpers.ts +10 -3
- package/src/exa/index.ts +1 -11
- package/src/exa/search.ts +1 -122
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/lsp/config.ts +1 -0
- package/src/lsp/defaults.json +3 -3
- package/src/lsp/index.ts +4 -4
- package/src/lsp/utils.ts +81 -0
- package/src/modes/components/settings-defs.ts +5 -0
- package/src/modes/components/todo-reminder.ts +8 -1
- package/src/modes/controllers/command-controller.ts +77 -3
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +2 -3
- package/src/modes/controllers/selector-controller.ts +18 -17
- package/src/modes/interactive-mode.ts +11 -7
- package/src/modes/theme/theme.ts +30 -27
- package/src/modes/types.ts +2 -1
- package/src/patch/hashline.ts +123 -22
- package/src/prompts/system/eager-todo.md +13 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/code-search.md +45 -0
- package/src/prompts/tools/find.md +1 -0
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +26 -111
- package/src/prompts/tools/read.md +2 -2
- package/src/prompts/tools/todo-write.md +11 -1
- package/src/sdk.ts +20 -16
- package/src/session/agent-session.ts +85 -7
- package/src/session/streaming-output.ts +17 -54
- package/src/slash-commands/builtin-registry.ts +10 -2
- package/src/task/executor.ts +10 -19
- package/src/task/index.ts +8 -4
- package/src/task/render.ts +5 -10
- package/src/task/template.ts +4 -1
- package/src/task/types.ts +2 -0
- package/src/tools/ast-edit.ts +26 -7
- package/src/tools/ast-grep.ts +26 -9
- package/src/tools/exit-plan-mode.ts +6 -0
- package/src/tools/fetch.ts +37 -6
- package/src/tools/find.ts +13 -64
- package/src/tools/grep.ts +27 -10
- package/src/tools/output-meta.ts +10 -7
- package/src/tools/path-utils.ts +348 -0
- package/src/tools/read.ts +13 -26
- package/src/tools/todo-write.ts +27 -4
- package/src/utils/commit-message-generator.ts +27 -22
- package/src/utils/image-input.ts +1 -1
- package/src/utils/image-resize.ts +4 -4
- package/src/utils/title-generator.ts +36 -23
- package/src/utils/tool-choice.ts +28 -0
- package/src/web/parallel.ts +346 -0
- package/src/web/scrapers/youtube.ts +29 -0
- package/src/web/search/code-search.ts +385 -0
- package/src/web/search/index.ts +25 -280
- package/src/web/search/provider.ts +4 -1
- package/src/web/search/providers/parallel.ts +63 -0
- package/src/web/search/types.ts +29 -0
- package/src/exa/company.ts +0 -26
- package/src/exa/linkedin.ts +0 -26
package/src/tools/grep.ts
CHANGED
|
@@ -16,7 +16,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
|
|
|
16
16
|
import { resolveFileDisplayMode } from "../utils/file-display-mode";
|
|
17
17
|
import type { ToolSession } from ".";
|
|
18
18
|
import { formatFullOutputReference, type OutputMeta } from "./output-meta";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
combineSearchGlobs,
|
|
21
|
+
hasGlobPathChars,
|
|
22
|
+
parseSearchPath,
|
|
23
|
+
resolveMultiSearchPath,
|
|
24
|
+
resolveToCwd,
|
|
25
|
+
} from "./path-utils";
|
|
20
26
|
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
|
|
21
27
|
import { ToolError } from "./tool-errors";
|
|
22
28
|
import { toolResult } from "./tool-result";
|
|
@@ -107,7 +113,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
107
113
|
const effectiveMultiline = multiline ?? patternHasNewline;
|
|
108
114
|
|
|
109
115
|
const useHashLines = resolveFileDisplayMode(this.session).hashLines;
|
|
116
|
+
const formatScopePath = (targetPath: string): string => {
|
|
117
|
+
const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
|
|
118
|
+
return relative.length === 0 ? "." : relative;
|
|
119
|
+
};
|
|
110
120
|
let searchPath: string;
|
|
121
|
+
let scopePath: string;
|
|
111
122
|
let globFilter = glob?.trim() || undefined;
|
|
112
123
|
const internalRouter = this.session.internalRouter;
|
|
113
124
|
if (searchDir?.trim()) {
|
|
@@ -121,27 +132,33 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
|
121
132
|
throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
|
|
122
133
|
}
|
|
123
134
|
searchPath = resource.sourcePath;
|
|
135
|
+
scopePath = formatScopePath(searchPath);
|
|
124
136
|
} else {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
globFilter =
|
|
137
|
+
const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
|
|
138
|
+
if (multiSearchPath) {
|
|
139
|
+
searchPath = multiSearchPath.basePath;
|
|
140
|
+
globFilter = multiSearchPath.glob;
|
|
141
|
+
scopePath = multiSearchPath.scopePath;
|
|
142
|
+
} else {
|
|
143
|
+
const parsedPath = parseSearchPath(rawPath);
|
|
144
|
+
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
145
|
+
if (parsedPath.glob) {
|
|
146
|
+
globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
|
|
147
|
+
}
|
|
148
|
+
scopePath = formatScopePath(searchPath);
|
|
129
149
|
}
|
|
130
150
|
}
|
|
131
151
|
} else {
|
|
132
152
|
searchPath = resolveToCwd(".", this.session.cwd);
|
|
153
|
+
scopePath = ".";
|
|
133
154
|
}
|
|
134
|
-
const scopePath = (() => {
|
|
135
|
-
const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
136
|
-
return relative.length === 0 ? "." : relative;
|
|
137
|
-
})();
|
|
138
155
|
|
|
139
156
|
let isDirectory: boolean;
|
|
140
157
|
try {
|
|
141
158
|
const stat = await Bun.file(searchPath).stat();
|
|
142
159
|
isDirectory = stat.isDirectory();
|
|
143
160
|
} catch {
|
|
144
|
-
throw new ToolError(`Path not found: ${
|
|
161
|
+
throw new ToolError(`Path not found: ${scopePath}`);
|
|
145
162
|
}
|
|
146
163
|
|
|
147
164
|
const effectiveOutputMode = "content";
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AgentToolUpdateCallback,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
15
|
+
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
15
16
|
import type { Theme } from "../modes/theme/theme";
|
|
16
17
|
import type { OutputSummary, TruncationResult } from "../session/streaming-output";
|
|
17
18
|
import { formatBytes, wrapBrackets } from "./render-utils";
|
|
@@ -116,26 +117,28 @@ export class OutputMetaBuilder {
|
|
|
116
117
|
if (!result.truncated) return this;
|
|
117
118
|
|
|
118
119
|
const { direction, startLine = 1, totalFileLines, artifactId } = options;
|
|
120
|
+
const outputLines = result.outputLines ?? result.totalLines;
|
|
121
|
+
const outputBytes = result.outputBytes ?? result.totalBytes;
|
|
122
|
+
const truncatedBy: "lines" | "bytes" = result.truncatedBy === "lines" ? "lines" : "bytes";
|
|
119
123
|
|
|
120
124
|
let shownStart: number;
|
|
121
125
|
let shownEnd: number;
|
|
122
126
|
|
|
123
127
|
if (direction === "tail") {
|
|
124
|
-
shownStart = result.totalLines -
|
|
128
|
+
shownStart = result.totalLines - outputLines + 1;
|
|
125
129
|
shownEnd = result.totalLines;
|
|
126
130
|
} else {
|
|
127
131
|
shownStart = startLine;
|
|
128
|
-
shownEnd = startLine +
|
|
132
|
+
shownEnd = startLine + outputLines - 1;
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
this.#meta.truncation = {
|
|
132
136
|
direction,
|
|
133
|
-
truncatedBy
|
|
137
|
+
truncatedBy,
|
|
134
138
|
totalLines: totalFileLines ?? result.totalLines,
|
|
135
139
|
totalBytes: result.totalBytes,
|
|
136
|
-
outputLines
|
|
137
|
-
outputBytes
|
|
138
|
-
maxBytes: result.maxBytes,
|
|
140
|
+
outputLines,
|
|
141
|
+
outputBytes,
|
|
139
142
|
shownRange: { start: shownStart, end: shownEnd },
|
|
140
143
|
artifactId,
|
|
141
144
|
nextOffset: direction === "head" ? shownEnd + 1 : undefined,
|
|
@@ -386,7 +389,7 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
|
|
|
386
389
|
let diagnosticsNotice = "";
|
|
387
390
|
if (meta.diagnostics && meta.diagnostics.messages.length > 0) {
|
|
388
391
|
const d = meta.diagnostics;
|
|
389
|
-
diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n
|
|
392
|
+
diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${formatGroupedDiagnosticMessages(d.messages)}`;
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
const notice = parts.length ? `\n\n[${parts.join(". ")}]` : "";
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -4,6 +4,14 @@ import * as path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
6
6
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
|
7
|
+
const TOP_LEVEL_INTERNAL_URL_PREFIXES = [
|
|
8
|
+
"agent://",
|
|
9
|
+
"artifact://",
|
|
10
|
+
"skill://",
|
|
11
|
+
"rule://",
|
|
12
|
+
"local://",
|
|
13
|
+
"mcp://",
|
|
14
|
+
] as const;
|
|
7
15
|
|
|
8
16
|
function normalizeUnicodeSpaces(str: string): string {
|
|
9
17
|
return str.replace(UNICODE_SPACES, " ");
|
|
@@ -38,6 +46,15 @@ function fileExists(filePath: string): boolean {
|
|
|
38
46
|
}
|
|
39
47
|
}
|
|
40
48
|
|
|
49
|
+
async function pathExists(filePath: string): Promise<boolean> {
|
|
50
|
+
try {
|
|
51
|
+
await fs.promises.access(filePath, fs.constants.F_OK);
|
|
52
|
+
return true;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
function normalizeAtPrefix(filePath: string): string {
|
|
42
59
|
if (!filePath.startsWith("@")) return filePath;
|
|
43
60
|
|
|
@@ -105,6 +122,24 @@ export interface ParsedSearchPath {
|
|
|
105
122
|
glob?: string;
|
|
106
123
|
}
|
|
107
124
|
|
|
125
|
+
export interface ParsedFindPattern {
|
|
126
|
+
basePath: string;
|
|
127
|
+
globPattern: string;
|
|
128
|
+
hasGlob: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ResolvedMultiSearchPath {
|
|
132
|
+
basePath: string;
|
|
133
|
+
glob?: string;
|
|
134
|
+
scopePath: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ResolvedMultiFindPattern {
|
|
138
|
+
basePath: string;
|
|
139
|
+
globPattern: string;
|
|
140
|
+
scopePath: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
108
143
|
/**
|
|
109
144
|
* Split a user path into a base path + glob pattern for tools that delegate to
|
|
110
145
|
* APIs accepting separate `path` and `glob` arguments.
|
|
@@ -128,6 +163,44 @@ export function parseSearchPath(filePath: string): ParsedSearchPath {
|
|
|
128
163
|
};
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
// Parse a find pattern into a base directory path and a glob pattern.
|
|
167
|
+
// Examples:
|
|
168
|
+
// src/app/**/\*.tsx -> { basePath: "src/app", globPattern: "**/*.tsx", hasGlob: true }
|
|
169
|
+
// src/app/\*.tsx -> { basePath: "src/app", globPattern: "*.tsx", hasGlob: true }
|
|
170
|
+
// \*.ts -> { basePath: ".", globPattern: "**/*.ts", hasGlob: true }
|
|
171
|
+
// **/\*.json -> { basePath: ".", globPattern: "**/*.json", hasGlob: true }
|
|
172
|
+
// /abs/path/**/\*.ts -> { basePath: "/abs/path", globPattern: "**/*.ts", hasGlob: true }
|
|
173
|
+
// src/app -> { basePath: "src/app", globPattern: "**/*", hasGlob: false }
|
|
174
|
+
export function parseFindPattern(pattern: string): ParsedFindPattern {
|
|
175
|
+
const segments = pattern.split("/");
|
|
176
|
+
let firstGlobIndex = -1;
|
|
177
|
+
for (let i = 0; i < segments.length; i++) {
|
|
178
|
+
if (hasGlobPathChars(segments[i])) {
|
|
179
|
+
firstGlobIndex = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (firstGlobIndex === -1) {
|
|
185
|
+
return { basePath: pattern, globPattern: "**/*", hasGlob: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (firstGlobIndex === 0) {
|
|
189
|
+
const needsRecursive = !pattern.startsWith("**/");
|
|
190
|
+
return {
|
|
191
|
+
basePath: ".",
|
|
192
|
+
globPattern: needsRecursive ? `**/${pattern}` : pattern,
|
|
193
|
+
hasGlob: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
basePath: segments.slice(0, firstGlobIndex).join("/"),
|
|
199
|
+
globPattern: segments.slice(firstGlobIndex).join("/"),
|
|
200
|
+
hasGlob: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
export function combineSearchGlobs(prefixGlob?: string, suffixGlob?: string): string | undefined {
|
|
132
205
|
if (!prefixGlob) return suffixGlob;
|
|
133
206
|
if (!suffixGlob) return prefixGlob;
|
|
@@ -138,6 +211,281 @@ export function combineSearchGlobs(prefixGlob?: string, suffixGlob?: string): st
|
|
|
138
211
|
return `${normalizedPrefix}/${normalizedSuffix}`;
|
|
139
212
|
}
|
|
140
213
|
|
|
214
|
+
type TopLevelSeparator = "comma" | "whitespace";
|
|
215
|
+
|
|
216
|
+
function splitTopLevel(value: string, separator: TopLevelSeparator): string[] {
|
|
217
|
+
const parts: string[] = [];
|
|
218
|
+
let current = "";
|
|
219
|
+
let braceDepth = 0;
|
|
220
|
+
let bracketDepth = 0;
|
|
221
|
+
let parenDepth = 0;
|
|
222
|
+
let quote: '"' | "'" | undefined;
|
|
223
|
+
let escaped = false;
|
|
224
|
+
|
|
225
|
+
const pushCurrent = () => {
|
|
226
|
+
const normalized = current.trim();
|
|
227
|
+
if (normalized.length > 0) {
|
|
228
|
+
parts.push(normalized);
|
|
229
|
+
}
|
|
230
|
+
current = "";
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
for (const char of value) {
|
|
234
|
+
if (escaped) {
|
|
235
|
+
current += char;
|
|
236
|
+
escaped = false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (char === "\\") {
|
|
241
|
+
current += char;
|
|
242
|
+
escaped = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (quote) {
|
|
247
|
+
current += char;
|
|
248
|
+
if (char === quote) {
|
|
249
|
+
quote = undefined;
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (char === '"' || char === "'") {
|
|
255
|
+
quote = char;
|
|
256
|
+
current += char;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (char === "{") braceDepth += 1;
|
|
261
|
+
else if (char === "}" && braceDepth > 0) braceDepth -= 1;
|
|
262
|
+
else if (char === "[") bracketDepth += 1;
|
|
263
|
+
else if (char === "]" && bracketDepth > 0) bracketDepth -= 1;
|
|
264
|
+
else if (char === "(") parenDepth += 1;
|
|
265
|
+
else if (char === ")" && parenDepth > 0) parenDepth -= 1;
|
|
266
|
+
|
|
267
|
+
const topLevel = braceDepth === 0 && bracketDepth === 0 && parenDepth === 0;
|
|
268
|
+
const isWhitespace = /\s/.test(char);
|
|
269
|
+
if (topLevel && separator === "comma" && char === ",") {
|
|
270
|
+
pushCurrent();
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (topLevel && separator === "whitespace" && isWhitespace) {
|
|
274
|
+
pushCurrent();
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
current += char;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
pushCurrent();
|
|
282
|
+
return parts.length > 1 ? parts : [value.trim()];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function normalizePosixPath(filePath: string): string {
|
|
286
|
+
return filePath.replace(/\\/g, "/");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function joinRelativeGlob(basePath: string | undefined, globPattern: string): string {
|
|
290
|
+
if (!basePath || basePath === ".") return normalizePosixPath(globPattern).replace(/^\/+/, "");
|
|
291
|
+
const normalizedBase = normalizePosixPath(basePath).replace(/\/+$/, "");
|
|
292
|
+
const normalizedGlob = normalizePosixPath(globPattern).replace(/^\/+/, "");
|
|
293
|
+
return `${normalizedBase}/${normalizedGlob}`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function buildBraceUnion(patterns: string[]): string | undefined {
|
|
297
|
+
const uniquePatterns = [...new Set(patterns.map(pattern => normalizePosixPath(pattern).trim()).filter(Boolean))];
|
|
298
|
+
if (uniquePatterns.length === 0) return undefined;
|
|
299
|
+
if (uniquePatterns.length === 1) return uniquePatterns[0];
|
|
300
|
+
return `{${uniquePatterns.join(",")}}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function findCommonBasePath(paths: string[]): string {
|
|
304
|
+
if (paths.length === 0) return ".";
|
|
305
|
+
let commonParts = path.resolve(paths[0]).split(path.sep);
|
|
306
|
+
for (const candidatePath of paths.slice(1)) {
|
|
307
|
+
const candidateParts = path.resolve(candidatePath).split(path.sep);
|
|
308
|
+
let sharedCount = 0;
|
|
309
|
+
const maxShared = Math.min(commonParts.length, candidateParts.length);
|
|
310
|
+
while (sharedCount < maxShared && commonParts[sharedCount] === candidateParts[sharedCount]) {
|
|
311
|
+
sharedCount += 1;
|
|
312
|
+
}
|
|
313
|
+
commonParts = commonParts.slice(0, sharedCount);
|
|
314
|
+
}
|
|
315
|
+
if (commonParts.length === 0) {
|
|
316
|
+
return path.parse(path.resolve(paths[0])).root;
|
|
317
|
+
}
|
|
318
|
+
const joined = commonParts.join(path.sep);
|
|
319
|
+
return joined || path.parse(path.resolve(paths[0])).root;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function toScopeDisplay(items: string[]): string {
|
|
323
|
+
return items.map(item => normalizePosixPath(item)).join(", ");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function looksLikeDelimitedPathToken(token: string): boolean {
|
|
327
|
+
return (
|
|
328
|
+
TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix)) ||
|
|
329
|
+
token.startsWith(".") ||
|
|
330
|
+
token.startsWith("/") ||
|
|
331
|
+
token.startsWith("~") ||
|
|
332
|
+
token.startsWith("@") ||
|
|
333
|
+
token.includes("/") ||
|
|
334
|
+
token.includes("\\") ||
|
|
335
|
+
hasGlobPathChars(token) ||
|
|
336
|
+
/\.[^./\\]+$/.test(token)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function areDelimitedTokensResolvable(
|
|
341
|
+
tokens: string[],
|
|
342
|
+
cwd: string,
|
|
343
|
+
parseBasePath: (value: string) => string,
|
|
344
|
+
allowBareExistingTokens: boolean,
|
|
345
|
+
): Promise<boolean> {
|
|
346
|
+
for (const token of tokens) {
|
|
347
|
+
if (TOP_LEVEL_INTERNAL_URL_PREFIXES.some(prefix => token.startsWith(prefix))) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (!allowBareExistingTokens && !looksLikeDelimitedPathToken(token)) {
|
|
352
|
+
// Bare names like "packages" don't look like path tokens syntactically,
|
|
353
|
+
// but may still be valid directory names. Check existence before rejecting.
|
|
354
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
355
|
+
if (!(await pathExists(resolvedExactPath))) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const basePath = parseBasePath(token);
|
|
362
|
+
const resolvedBasePath = resolveToCwd(basePath, cwd);
|
|
363
|
+
if (await pathExists(resolvedBasePath)) {
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!allowBareExistingTokens) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const resolvedExactPath = resolveToCwd(token, cwd);
|
|
372
|
+
if (!(await pathExists(resolvedExactPath))) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function splitDelimitedSearchInput(
|
|
381
|
+
rawInput: string,
|
|
382
|
+
cwd: string,
|
|
383
|
+
parseBasePath: (value: string) => string,
|
|
384
|
+
): Promise<string[] | undefined> {
|
|
385
|
+
const trimmed = rawInput.trim();
|
|
386
|
+
if (!trimmed) return undefined;
|
|
387
|
+
|
|
388
|
+
const resolvedExactPath = resolveToCwd(trimmed, cwd);
|
|
389
|
+
if (await pathExists(resolvedExactPath)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const commaSeparated = splitTopLevel(trimmed, "comma");
|
|
394
|
+
if (commaSeparated.length > 1 && (await areDelimitedTokensResolvable(commaSeparated, cwd, parseBasePath, true))) {
|
|
395
|
+
return [...new Set(commaSeparated)];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const whitespaceSeparated = splitTopLevel(trimmed, "whitespace");
|
|
399
|
+
if (
|
|
400
|
+
whitespaceSeparated.length > 1 &&
|
|
401
|
+
(await areDelimitedTokensResolvable(whitespaceSeparated, cwd, parseBasePath, false))
|
|
402
|
+
) {
|
|
403
|
+
return [...new Set(whitespaceSeparated)];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export async function resolveMultiSearchPath(
|
|
410
|
+
rawPath: string,
|
|
411
|
+
cwd: string,
|
|
412
|
+
suffixGlob?: string,
|
|
413
|
+
): Promise<ResolvedMultiSearchPath | undefined> {
|
|
414
|
+
const pathItems = await splitDelimitedSearchInput(rawPath, cwd, value => parseSearchPath(value).basePath);
|
|
415
|
+
if (!pathItems || pathItems.length <= 1) {
|
|
416
|
+
return undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const parsedItems = await Promise.all(
|
|
420
|
+
pathItems.map(async item => {
|
|
421
|
+
const parsedPath = parseSearchPath(item);
|
|
422
|
+
const absoluteBasePath = resolveToCwd(parsedPath.basePath, cwd);
|
|
423
|
+
const stat = await fs.promises.stat(absoluteBasePath);
|
|
424
|
+
return { raw: item, parsedPath, absoluteBasePath, stat };
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
429
|
+
const combinedPatterns = parsedItems.map(item => {
|
|
430
|
+
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
431
|
+
if (item.parsedPath.glob) {
|
|
432
|
+
const pathGlob = joinRelativeGlob(relativeBasePath, item.parsedPath.glob);
|
|
433
|
+
return combineSearchGlobs(pathGlob, suffixGlob) ?? pathGlob;
|
|
434
|
+
}
|
|
435
|
+
if (suffixGlob) {
|
|
436
|
+
const pathPrefix = relativeBasePath === "." ? undefined : relativeBasePath;
|
|
437
|
+
return combineSearchGlobs(pathPrefix, suffixGlob) ?? suffixGlob;
|
|
438
|
+
}
|
|
439
|
+
if (item.stat.isDirectory()) {
|
|
440
|
+
return joinRelativeGlob(relativeBasePath, "**/*");
|
|
441
|
+
}
|
|
442
|
+
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
basePath: commonBasePath,
|
|
447
|
+
glob: buildBraceUnion(combinedPatterns),
|
|
448
|
+
scopePath: toScopeDisplay(pathItems),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
export async function resolveMultiFindPattern(
|
|
453
|
+
rawPattern: string,
|
|
454
|
+
cwd: string,
|
|
455
|
+
): Promise<ResolvedMultiFindPattern | undefined> {
|
|
456
|
+
const patternItems = await splitDelimitedSearchInput(rawPattern, cwd, value => parseFindPattern(value).basePath);
|
|
457
|
+
if (!patternItems || patternItems.length <= 1) {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const parsedItems = await Promise.all(
|
|
462
|
+
patternItems.map(async item => {
|
|
463
|
+
const parsedPattern = parseFindPattern(item);
|
|
464
|
+
const absoluteBasePath = resolveToCwd(parsedPattern.basePath, cwd);
|
|
465
|
+
const stat = await fs.promises.stat(absoluteBasePath);
|
|
466
|
+
return { raw: item, parsedPattern, absoluteBasePath, stat };
|
|
467
|
+
}),
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const commonBasePath = findCommonBasePath(parsedItems.map(item => item.absoluteBasePath));
|
|
471
|
+
const combinedPatterns = parsedItems.map(item => {
|
|
472
|
+
const relativeBasePath = normalizePosixPath(path.relative(commonBasePath, item.absoluteBasePath)) || ".";
|
|
473
|
+
if (item.parsedPattern.hasGlob) {
|
|
474
|
+
return joinRelativeGlob(relativeBasePath, item.parsedPattern.globPattern);
|
|
475
|
+
}
|
|
476
|
+
if (item.stat.isDirectory()) {
|
|
477
|
+
return joinRelativeGlob(relativeBasePath, "**/*");
|
|
478
|
+
}
|
|
479
|
+
return relativeBasePath === "." ? path.basename(item.absoluteBasePath) : relativeBasePath;
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
basePath: commonBasePath,
|
|
484
|
+
globPattern: buildBraceUnion(combinedPatterns) ?? "**/*",
|
|
485
|
+
scopePath: toScopeDisplay(patternItems),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
141
489
|
export function resolveReadPath(filePath: string, cwd: string): string {
|
|
142
490
|
const resolved = resolveToCwd(filePath, cwd);
|
|
143
491
|
const shellEscapedVariant = tryShellEscapedPath(resolved);
|
package/src/tools/read.ts
CHANGED
|
@@ -16,6 +16,7 @@ import type { ToolSession } from "../sdk";
|
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_MAX_BYTES,
|
|
18
18
|
DEFAULT_MAX_LINES,
|
|
19
|
+
noTruncResult,
|
|
19
20
|
type TruncationResult,
|
|
20
21
|
truncateHead,
|
|
21
22
|
truncateHeadBytes,
|
|
@@ -592,15 +593,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
592
593
|
const truncation: TruncationResult = {
|
|
593
594
|
content: selectedContent,
|
|
594
595
|
truncated: wasTruncated,
|
|
595
|
-
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" :
|
|
596
|
+
truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : undefined,
|
|
596
597
|
totalLines: totalSelectedLines,
|
|
597
598
|
totalBytes: totalSelectedBytes,
|
|
598
599
|
outputLines: collectedLines.length,
|
|
599
600
|
outputBytes: collectedBytes,
|
|
600
601
|
lastLinePartial: false,
|
|
601
602
|
firstLineExceedsLimit,
|
|
602
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
603
|
-
maxBytes: DEFAULT_MAX_BYTES,
|
|
604
603
|
};
|
|
605
604
|
|
|
606
605
|
const shouldAddHashLines = displayMode.hashLines;
|
|
@@ -687,8 +686,6 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
687
686
|
async #handleInternalUrl(url: string, offset?: number, limit?: number): Promise<AgentToolResult<ReadToolDetails>> {
|
|
688
687
|
const internalRouter = this.session.internalRouter!;
|
|
689
688
|
|
|
690
|
-
const displayMode = resolveFileDisplayMode(this.session);
|
|
691
|
-
|
|
692
689
|
// Check if URL has query extraction (agent:// only)
|
|
693
690
|
let parsed: URL;
|
|
694
691
|
try {
|
|
@@ -716,7 +713,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
716
713
|
return toolResult(details).text(resource.content).sourceInternal(url).done();
|
|
717
714
|
}
|
|
718
715
|
|
|
719
|
-
// Apply pagination similar to file reading
|
|
716
|
+
// Apply pagination similar to file reading.
|
|
720
717
|
const allLines = resource.content.split("\n");
|
|
721
718
|
const totalLines = allLines.length;
|
|
722
719
|
|
|
@@ -733,9 +730,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
733
730
|
.done();
|
|
734
731
|
}
|
|
735
732
|
|
|
733
|
+
const ignoreLimits = scheme === "skill";
|
|
736
734
|
let selectedContent: string;
|
|
737
735
|
let userLimitedLines: number | undefined;
|
|
738
|
-
if (limit !== undefined) {
|
|
736
|
+
if (limit !== undefined && !ignoreLimits) {
|
|
739
737
|
const endLine = Math.min(startLine + limit, allLines.length);
|
|
740
738
|
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
741
739
|
userLimitedLines = endLine - startLine;
|
|
@@ -743,14 +741,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
743
741
|
selectedContent = allLines.slice(startLine).join("\n");
|
|
744
742
|
}
|
|
745
743
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const shouldAddHashLines = displayMode.hashLines;
|
|
750
|
-
const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
|
|
751
|
-
const formatText = (text: string, startNum: number): string => {
|
|
752
|
-
return formatTextWithMode(text, startNum, shouldAddHashLines, shouldAddLineNumbers);
|
|
753
|
-
};
|
|
744
|
+
const truncation: TruncationResult = ignoreLimits
|
|
745
|
+
? noTruncResult(selectedContent)
|
|
746
|
+
: truncateHead(selectedContent);
|
|
754
747
|
|
|
755
748
|
let outputText: string;
|
|
756
749
|
let truncationInfo:
|
|
@@ -762,13 +755,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
762
755
|
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
763
756
|
const snippet = truncateHeadBytes(firstLine, DEFAULT_MAX_BYTES);
|
|
764
757
|
|
|
765
|
-
|
|
766
|
-
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
767
|
-
firstLineBytes,
|
|
768
|
-
)}, exceeds ${formatBytes(DEFAULT_MAX_BYTES)} limit. Hashline output requires full lines; cannot compute hashes for a truncated preview.]`;
|
|
769
|
-
} else {
|
|
770
|
-
outputText = formatText(snippet.text, startLineDisplay);
|
|
771
|
-
}
|
|
758
|
+
outputText = snippet.text;
|
|
772
759
|
if (snippet.text.length === 0) {
|
|
773
760
|
outputText = `[Line ${startLineDisplay} is ${formatBytes(
|
|
774
761
|
firstLineBytes,
|
|
@@ -780,7 +767,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
780
767
|
options: { direction: "head", startLine: startLineDisplay, totalFileLines: totalLines },
|
|
781
768
|
};
|
|
782
769
|
} else if (truncation.truncated) {
|
|
783
|
-
outputText =
|
|
770
|
+
outputText = truncation.content;
|
|
784
771
|
details.truncation = truncation;
|
|
785
772
|
truncationInfo = {
|
|
786
773
|
result: truncation,
|
|
@@ -790,11 +777,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
790
777
|
const remaining = allLines.length - (startLine + userLimitedLines);
|
|
791
778
|
const nextOffset = startLine + userLimitedLines + 1;
|
|
792
779
|
|
|
793
|
-
outputText =
|
|
780
|
+
outputText = truncation.content;
|
|
794
781
|
outputText += `\n\n[${remaining} more lines in resource. Use offset=${nextOffset} to continue]`;
|
|
795
782
|
details.truncation = truncation;
|
|
796
783
|
} else {
|
|
797
|
-
outputText =
|
|
784
|
+
outputText = truncation.content;
|
|
798
785
|
}
|
|
799
786
|
|
|
800
787
|
const resultBuilder = toolResult(details).text(outputText).sourceInternal(url);
|
|
@@ -924,7 +911,7 @@ export const readToolRenderer = {
|
|
|
924
911
|
}
|
|
925
912
|
if (truncation) {
|
|
926
913
|
if (fallback?.firstLineExceedsLimit) {
|
|
927
|
-
let warning = `First line exceeds ${formatBytes(fallback.
|
|
914
|
+
let warning = `First line exceeds ${formatBytes(fallback.outputBytes ?? fallback.totalBytes)} limit`;
|
|
928
915
|
if (truncation.artifactId) {
|
|
929
916
|
warning += `. ${formatFullOutputReference(truncation.artifactId)}`;
|
|
930
917
|
}
|