can1357/oh-my-pi · error · ToolError

Line-range selector requires a single file: ${spec.original}

Error message

Line-range selector requires a single file: ${spec.original} is a directory

What it means

A line-range selector (`path:start-end`) must point at a single regular file because ranges are line-based. The stat succeeded but showed a directory, so the tool rejects it rather than silently ignoring the range.

Source

Thrown at packages/coding-agent/src/tools/grep.ts:1083

						const spec = pathSpecs[idx];
						if (!spec.ranges) continue;
						if (virtualInputIndexes.has(idx)) continue;
						const resolved = internalResolution.resolvedPathsByInput[idx];
						if (!resolved) continue;
						const materializedExternalPath = materializedExternalPaths.get(spec.clean);
						if (materializedExternalPath) {
							mergeRangesInto(rangesByAbsPath, path.resolve(materializedExternalPath), spec.ranges);
							continue;
						}
						if (resolved === spec.clean && !archiveDisplayMap.has(resolved)) {
							// Non-archive entry; ensure the cleaned path resolves to a regular file.
							const absKey = path.resolve(resolveReadPath(resolved, this.session.cwd));
							const stats = await stat(absKey).catch(() => null);
							if (!stats) {
								throw new ToolError(`Path not found for line-range selector: ${spec.original}`);
							}
							if (!stats.isFile()) {
								throw new ToolError(
									`Line-range selector requires a single file: ${spec.original} is a directory`,
								);
							}
							mergeRangesInto(rangesByAbsPath, absKey, spec.ranges);
						} else {
							mergeRangesInto(rangesByAbsPath, path.resolve(resolved), spec.ranges);
						}
					}
					// When the only input was an archive selector, surface that selector instead
					// of the temp scratch path the resolver substituted in.
					const physicalScopePath =
						searchablePaths.length === 1 && archiveDisplayMap.get(searchPath)
							? (archiveDisplayMap.get(searchPath) as string)
							: scope.scopePath;
					scopePath = internalResolution.virtualScopePath
						? `${physicalScopePath}, ${internalResolution.virtualScopePath}`
						: physicalScopePath;
				} else {

View on GitHub (pinned to 9690622007)

Solutions

  1. Point the range selector at a specific file inside the directory (src/components/Button.tsx:10-20)
  2. To search the directory, drop the range and use the `glob` option to filter files
  3. Read the target file first to confirm it is a file, not a directory

Example fix

// before
await grep({ pattern: "useState", path: "src/hooks:1-40" });
// after
await grep({ pattern: "useState", path: "src/hooks/useAuth.ts:1-40" });
Defensive patterns

Strategy: validation

Validate before calling

import * as fs from "node:fs";
const [file] = spec.split(":");
const st = fs.statSync(file);
if (st.isDirectory()) throw new Error(`Line ranges need a file, got directory: ${file}`);

Prevention

When it happens

Trigger: grep with path like `src/:10-20` or `src/components:1-100` — a directory path carrying line ranges.

Common situations: Mechanically appending :N-M to whatever path a variable holds; intending "lines 10-20 of every file in the dir", which this tool does not support via a range selector.

Related errors


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/fca06c55b20f3463. Report an issue: GitHub.