@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.
Files changed (76) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/package.json +7 -7
  3. package/src/commit/agentic/agent.ts +3 -1
  4. package/src/commit/agentic/index.ts +7 -1
  5. package/src/commit/analysis/conventional.ts +5 -1
  6. package/src/commit/analysis/summary.ts +5 -1
  7. package/src/commit/changelog/generate.ts +5 -1
  8. package/src/commit/changelog/index.ts +4 -0
  9. package/src/commit/map-reduce/index.ts +5 -0
  10. package/src/commit/map-reduce/map-phase.ts +17 -2
  11. package/src/commit/map-reduce/reduce-phase.ts +5 -1
  12. package/src/commit/model-selection.ts +38 -26
  13. package/src/commit/pipeline.ts +22 -11
  14. package/src/config/model-registry.ts +98 -17
  15. package/src/config/settings-schema.ts +31 -12
  16. package/src/config.ts +10 -3
  17. package/src/discovery/helpers.ts +10 -3
  18. package/src/exa/index.ts +1 -11
  19. package/src/exa/search.ts +1 -122
  20. package/src/internal-urls/docs-index.generated.ts +2 -2
  21. package/src/lsp/config.ts +1 -0
  22. package/src/lsp/defaults.json +3 -3
  23. package/src/lsp/index.ts +4 -4
  24. package/src/lsp/utils.ts +81 -0
  25. package/src/modes/components/settings-defs.ts +5 -0
  26. package/src/modes/components/todo-reminder.ts +8 -1
  27. package/src/modes/controllers/command-controller.ts +77 -3
  28. package/src/modes/controllers/extension-ui-controller.ts +6 -0
  29. package/src/modes/controllers/input-controller.ts +2 -3
  30. package/src/modes/controllers/selector-controller.ts +18 -17
  31. package/src/modes/interactive-mode.ts +11 -7
  32. package/src/modes/theme/theme.ts +30 -27
  33. package/src/modes/types.ts +2 -1
  34. package/src/patch/hashline.ts +123 -22
  35. package/src/prompts/system/eager-todo.md +13 -0
  36. package/src/prompts/tools/ast-edit.md +1 -1
  37. package/src/prompts/tools/ast-grep.md +1 -1
  38. package/src/prompts/tools/code-search.md +45 -0
  39. package/src/prompts/tools/find.md +1 -0
  40. package/src/prompts/tools/grep.md +1 -0
  41. package/src/prompts/tools/hashline.md +26 -111
  42. package/src/prompts/tools/read.md +2 -2
  43. package/src/prompts/tools/todo-write.md +11 -1
  44. package/src/sdk.ts +20 -16
  45. package/src/session/agent-session.ts +85 -7
  46. package/src/session/streaming-output.ts +17 -54
  47. package/src/slash-commands/builtin-registry.ts +10 -2
  48. package/src/task/executor.ts +10 -19
  49. package/src/task/index.ts +8 -4
  50. package/src/task/render.ts +5 -10
  51. package/src/task/template.ts +4 -1
  52. package/src/task/types.ts +2 -0
  53. package/src/tools/ast-edit.ts +26 -7
  54. package/src/tools/ast-grep.ts +26 -9
  55. package/src/tools/exit-plan-mode.ts +6 -0
  56. package/src/tools/fetch.ts +37 -6
  57. package/src/tools/find.ts +13 -64
  58. package/src/tools/grep.ts +27 -10
  59. package/src/tools/output-meta.ts +10 -7
  60. package/src/tools/path-utils.ts +348 -0
  61. package/src/tools/read.ts +13 -26
  62. package/src/tools/todo-write.ts +27 -4
  63. package/src/utils/commit-message-generator.ts +27 -22
  64. package/src/utils/image-input.ts +1 -1
  65. package/src/utils/image-resize.ts +4 -4
  66. package/src/utils/title-generator.ts +36 -23
  67. package/src/utils/tool-choice.ts +28 -0
  68. package/src/web/parallel.ts +346 -0
  69. package/src/web/scrapers/youtube.ts +29 -0
  70. package/src/web/search/code-search.ts +385 -0
  71. package/src/web/search/index.ts +25 -280
  72. package/src/web/search/provider.ts +4 -1
  73. package/src/web/search/providers/parallel.ts +63 -0
  74. package/src/web/search/types.ts +29 -0
  75. package/src/exa/company.ts +0 -26
  76. 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 { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
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 parsedPath = parseSearchPath(rawPath);
126
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
127
- if (parsedPath.glob) {
128
- globFilter = combineSearchGlobs(parsedPath.glob, 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: ${searchPath}`);
161
+ throw new ToolError(`Path not found: ${scopePath}`);
145
162
  }
146
163
 
147
164
  const effectiveOutputMode = "content";
@@ -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 - result.outputLines + 1;
128
+ shownStart = result.totalLines - outputLines + 1;
125
129
  shownEnd = result.totalLines;
126
130
  } else {
127
131
  shownStart = startLine;
128
- shownEnd = startLine + result.outputLines - 1;
132
+ shownEnd = startLine + outputLines - 1;
129
133
  }
130
134
 
131
135
  this.#meta.truncation = {
132
136
  direction,
133
- truncatedBy: result.truncatedBy!,
137
+ truncatedBy,
134
138
  totalLines: totalFileLines ?? result.totalLines,
135
139
  totalBytes: result.totalBytes,
136
- outputLines: result.outputLines,
137
- outputBytes: result.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 ${d.messages.join("\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(". ")}]` : "";
@@ -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" : null,
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
- // Apply truncation
747
- const truncation = truncateHead(selectedContent);
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
- if (shouldAddHashLines) {
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 = formatText(truncation.content, startLineDisplay);
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 = formatText(truncation.content, startLineDisplay);
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 = formatText(truncation.content, startLineDisplay);
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.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
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
  }