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
- Point the range selector at a specific file inside the directory (src/components/Button.tsx:10-20)
- To search the directory, drop the range and use the `glob` option to filter files
- 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
- Only append :N-M to known file paths
- Use the glob option for directory-wide filtering instead of range selectors
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
- grep cannot recurse the directory listing at ${rawPath}; gre
- Path not found for line-range selector: ${spec.original}
- Is a directory
- {}: hard link not allowed for directory
- Directory paths are not supported by read(): ${filePath}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/fca06c55b20f3463.
Report an issue: GitHub.