feat: add optional scope path-prefix filter to list_files and list CLI (#165)#167
Merged
Conversation
Pure JS helper (isUnderOrEqual/matchesAnyScope) mirroring the SQL buildPrefixPredicate contract: separator derived from the prefix (default node:path.sep), trailing-separator equivalence, boundary-safe exact-or-descendant matching. Foundation for list_files/list scope (Phase 1 T1, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread an optional scope into scanBaseDir (3rd param) and bfsCollectSupportedFiles (4th param, after maxDepth). A shouldVisitDir gate (matchesAnyScope OR ancestor-of-scope) is applied at the root enqueue and before each subdir enqueue, so a non-intersecting baseDir is skipped with zero readdir; files are collected only when in scope. Scope-absent behavior is byte-for-byte unchanged (Phase 1 T2, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Walker-layer integration test run against both scanBaseDir and bfsCollectSupportedFiles: a readdir EACCES-mock/spy records visited dirs and asserts a scope-outside path is never visited under scope (with a no-scope sentinel proving the probe fires) — discriminating a post-scan filter from real pruning. Covers boundary, root gate, ancestor descent, order, cross-platform separators, and AC10 warning semantics (Phase 1 T3, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the MCP-boundary type and validator for list_files scope.
parseListFilesInput preserves the no-argument contract: undefined/{}
short-circuit to no-scope before asRecord (AC12), and a present scope
is validated via the existing module-private normalizeScope. Not yet
wired into the dispatcher (Phase 2 T1, #165).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fier Wire the list_files MCP dispatch through parseListFilesInput and thread the normalized scope into scanBaseDir. Extract the sources classification into a new pure, surface-independent helper classifyIngestedSources (src/utils/list-sources.ts): raw-data sources are always emitted; real-file entries are scope-filtered so a scope-outside ingested real file drops from both files[] and sources[]. Scope-absent behavior is byte-for-byte unchanged. Adds INT-1 handler integration tests (real LanceDB + FS; scanned-file realpath spy proving pushdown; symlink-alias contract) and list-sources unit tests (Phase 2 T2, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional scope property (oneOf string|array) to the list_files inputSchema with bespoke scan-path-basis wording — files reachable at a path equal to or under a prefix within the base directories — instead of copying the query_documents "filePath equal to or under" phrasing, which would contradict the scan-path guarantee under aliased roots (Phase 3 T1, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse a repeatable --scope <prefix> (empty/whitespace rejected with a non-zero exit), thread it into scanRoot -> bfsCollectSupportedFiles, and reuse the shared classifyIngestedSources so the CLI and MCP surfaces apply an identical raw-data/real-file sources split. Document --scope in HELP_TEXT. Adds INT-3 integration tests (real LanceDB + FS; scanned-file realpath spy pushdown proof; platform-guarded alias) (Phase 3 T2, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a service-integration-e2e (E2E-1) that spawns the real CLI `list --scope` against a real LanceDB + FS fixture and asserts the stdout JSON files[] are scope-restricted, raw-data sources retained, and repeated --scope unions. Document list_files / list --scope (scan-path basis) in README, SKILL.md, and cli-reference.md (Final Phase, #165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the duplicated scope-integration composition (shouldVisitDir / file-collect guard) from both BFS walkers into pure shouldVisitDir and isInScope helpers in scope-match.ts. Removes the drift-prone core of the walker duplication (code-review finding #1) so a boundary-semantics change lives in one place. Behavior-preserving; INT-2 covers both walkers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A non-absolute scope prefix matches nothing under the exact-or-descendant contract, which previously produced a silent empty result. Both surfaces now surface a non-fatal warning naming each non-absolute prefix (MCP: a warning content block; CLI: a stderr Warning, exit 0 preserved) so the "why is it empty?" gap is closed. Result semantics unchanged; the existing empty/whitespace --scope rejection is untouched (code-review finding #2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The list_files schema — the only description an MCP client reads at call time — under-documented sources and scope's effect on them, which read as a bug during MCP debugging. Name sources as ingest_data content (dropping the imprecise "outside the base dirs"), and state positively that scope filters files by their scan path while ingest_data sources (which have no base-directory path) are always listed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Minor release: adds the optional scope path-prefix filter to the list_files tool and the list CLI subcommand (#165). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SKILL.md's `Search: Core Rules > Scope` section documents query_documents scope; the appended list_files/CLI paragraph belonged elsewhere. CLI flags live in references/cli-reference.md, and the scope-vs-sources behavior is now stated in the list_files tool schema description itself, so the supplement is redundant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the restated boundary example and verbose phrasing so the list --scope row mirrors the query --scope entry's density, keeping only the two real deltas: scan-path basis and ingest_data sources always listed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /a/b/ and /a/b// variants appended a hardcoded "/", producing a mixed "\...\a\b/" prefix on Windows that the matcher reads as \-style (and never strips), so the case returned empty. Build the variants with the OS separator instead. Test-only; production is unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an optional
scopepath-prefix filter to thelist_filesMCP tool and thelistCLI subcommand, resolving the large-volume timeout reported in #165.scopeis one absolute path prefix or a list (unioned), matched exact-or-descendant on each file's on-disk scan path — reusing the semantics locked forquery_documentsin #146.The origin is a real timeout: on a volume with several thousand files,
list_filesenumerates the whole tree. Rather than filter the result after the fact (which would leave the walk cost intact), the scope predicate is pushed into the BFS traversal — a directory outside every prefix is neverreaddir'd — so a scoped listing costs work proportional to the scoped subtree instead of the whole volume.Minor release:
0.15.4→0.16.0. Fully backward compatible — omittingscope(and callinglist_fileswith no arguments) behaves exactly as before.Behavior
files[]is restricted to entries whose scan path is equal to or under a prefix; boundary-safe (/docs/apimatches/docs/api/x.md, not/docs/apiv2).sources:ingest_data(raw-data) sources are always listed regardless of scope; real-file source entries respect scope. This split is applied by a sharedclassifyIngestedSourceshelper used by both surfaces.Warning, exit 0) so an empty result is explained rather than silent.scope: rejected at the boundary (MCPMcpError(InvalidParams); CLI non-zero exit).list_files(argumentsomitted /{}) still returns the full listing — no regression.Path basis
scopeis guaranteed over the on-disk reachable (scan) path, which makes the traversal pushdown exact. Under a symlinked/aliased root the returnedfilePathkeeps its stored spelling while scope matches the reachable path; this edge is defined and fixture-covered.Implementation
src/utils/scope-match.ts(new) — pure boundary-safe matcher (isUnderOrEqual,matchesAnyScope,shouldVisitDir,isInScope,nonAbsolutePrefixes), the JS counterpart of the SQLbuildPrefixPredicate.src/utils/list-sources.ts(new) — pureclassifyIngestedSourcesshared by the MCP handler and the CLI (removes a prior byte-for-byte duplication).scanBaseDir,bfsCollectSupportedFiles) with a root gate before the firstreaddir.ListFilesInput+parseListFilesInput(acceptsundefined/{}as no-scope), scope on thelist_filesschema with scan-path-basis wording, warning content block.--scope, threading, andHELP_TEXT.sourcesareingest_dataitems and that scope filters files (notingest_datasources).Tests
scope-match,list-sources,parseListFilesInput, schema.readdirEACCES mock/spy (a scope-outside path is never visited), discriminating real pruning from a post-scan filter.realpathspy.list --scopeagainst a real fixture.pnpm run check:allgreen (1047 tests); cross-platform-sensitive fixtures (symlink alias) skip on unsupported platforms rather than fail.Closes #165.
🤖 Generated with Claude Code