Skip to content

Commit 7366803

Browse files
committed
✨Moved the gitignore-aware ** traversal into the kaos package and switched the glob tool to call it, so the matching logic lives in packages/kaos rather than being inlined in Kimi CLI. This removes the local matching implementation while keeping the same behavior. Updated packages/kaos/src/kaos/__init__.py and src/kimi_cli/tools/file/glob.py.
1 parent 1ffe7f5 commit 7366803

File tree

2 files changed

+76
-62
lines changed

2 files changed

+76
-62
lines changed

packages/kaos/src/kaos/__init__.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from __future__ import annotations
22

33
import contextvars
4-
from collections.abc import AsyncGenerator, AsyncIterator, Iterable
4+
import fnmatch
5+
from collections.abc import AsyncGenerator, AsyncIterator, Callable, Iterable
56
from dataclasses import dataclass
6-
from pathlib import PurePath
7+
from pathlib import PurePath, PurePosixPath
78
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable
89

910
if TYPE_CHECKING:
@@ -294,6 +295,70 @@ def glob(
294295
return get_current_kaos().glob(path, pattern, case_sensitive=case_sensitive)
295296

296297

298+
async def glob_pruned(
299+
base_dir: KaosPath,
300+
pattern: str,
301+
*,
302+
should_ignore: Callable[[KaosPath, bool], bool] | None = None,
303+
) -> list[KaosPath]:
304+
"""Glob with a pruning callback to skip entries during traversal."""
305+
normalized_pattern = pattern.replace("\\", "/")
306+
parts = list(PurePosixPath(normalized_pattern).parts)
307+
if parts and parts[0] == "/":
308+
parts = parts[1:]
309+
310+
matches: list[KaosPath] = []
311+
312+
def _should_ignore(path: KaosPath, is_dir: bool) -> bool:
313+
if should_ignore is None:
314+
return False
315+
return should_ignore(path, is_dir)
316+
317+
async def recurse(current: KaosPath, idx: int) -> None:
318+
if idx == len(parts):
319+
matches.append(current)
320+
return
321+
322+
part = parts[idx]
323+
if part == "**":
324+
await recurse(current, idx + 1)
325+
326+
if not await current.is_dir():
327+
return
328+
329+
async for child in current.iterdir():
330+
try:
331+
is_dir = await child.is_dir()
332+
except OSError:
333+
continue
334+
335+
if _should_ignore(child, is_dir):
336+
continue
337+
338+
if is_dir:
339+
await recurse(child, idx)
340+
elif idx == len(parts) - 1:
341+
matches.append(child)
342+
return
343+
344+
if not await current.is_dir():
345+
return
346+
347+
async for child in current.iterdir():
348+
try:
349+
is_dir = await child.is_dir()
350+
except OSError:
351+
continue
352+
353+
if _should_ignore(child, is_dir):
354+
continue
355+
356+
if fnmatch.fnmatchcase(child.name, part):
357+
await recurse(child, idx + 1)
358+
359+
await recurse(base_dir, 0)
360+
return matches
361+
297362
async def readbytes(path: StrOrKaosPath, n: int | None = None) -> bytes:
298363
return await get_current_kaos().readbytes(path, n=n)
299364

src/kimi_cli/tools/file/glob.py

Lines changed: 9 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
"""Glob tool implementation."""
22

3-
import fnmatch
4-
from pathlib import Path, PurePosixPath
3+
from pathlib import Path
54
from typing import override
65

6+
from kaos import glob_pruned
77
from kaos.path import KaosPath
88
from kosong.tooling import CallableTool2, ToolError, ToolOk, ToolReturnValue
99
from pathspec import PathSpec
1010
from pydantic import BaseModel, Field
1111

12+
from kaos import glob_pruned
1213
from kimi_cli.soul.agent import BuiltinSystemPromptArgs
1314
from kimi_cli.tools.utils import load_desc
1415
from kimi_cli.utils.path import is_within_directory, list_directory
@@ -67,62 +68,6 @@ def _is_gitignored(self, path: KaosPath, gitignore_spec: PathSpec, is_dir: bool)
6768
relative_str += "/"
6869
return gitignore_spec.match_file(relative_str)
6970

70-
async def _gitignore_aware_glob(
71-
self, base_dir: KaosPath, pattern: str, gitignore_spec: PathSpec
72-
) -> list[KaosPath]:
73-
"""Glob that prunes gitignored directories instead of filtering after traversal."""
74-
normalized_pattern = pattern.replace("\\", "/")
75-
parts = list(PurePosixPath(normalized_pattern).parts)
76-
if parts and parts[0] == "/":
77-
parts = parts[1:]
78-
79-
matches: list[KaosPath] = []
80-
81-
async def recurse(current: KaosPath, idx: int) -> None:
82-
if idx == len(parts):
83-
matches.append(current)
84-
return
85-
86-
part = parts[idx]
87-
if part == "**":
88-
await recurse(current, idx + 1)
89-
90-
if not await current.is_dir():
91-
return
92-
93-
async for child in current.iterdir():
94-
try:
95-
is_dir = await child.is_dir()
96-
except OSError:
97-
continue
98-
99-
if self._is_gitignored(child, gitignore_spec, is_dir):
100-
continue
101-
102-
if is_dir:
103-
await recurse(child, idx)
104-
elif idx == len(parts) - 1:
105-
matches.append(child)
106-
return
107-
108-
if not await current.is_dir():
109-
return
110-
111-
async for child in current.iterdir():
112-
try:
113-
is_dir = await child.is_dir()
114-
except OSError:
115-
continue
116-
117-
if self._is_gitignored(child, gitignore_spec, is_dir):
118-
continue
119-
120-
if fnmatch.fnmatchcase(child.name, part):
121-
await recurse(child, idx + 1)
122-
123-
await recurse(base_dir, 0)
124-
return matches
125-
12671
async def _filter_gitignored(
12772
self, paths: list[KaosPath], gitignore_spec: PathSpec
12873
) -> list[KaosPath]:
@@ -217,8 +162,12 @@ async def __call__(self, params: Params) -> ToolReturnValue:
217162
# Perform the glob search - users can use ** directly in pattern
218163
normalized_pattern = params.pattern.replace("\\", "/")
219164
if gitignore_spec and normalized_pattern.startswith("**"):
220-
matches = await self._gitignore_aware_glob(
221-
dir_path, normalized_pattern, gitignore_spec
165+
matches = await glob_pruned(
166+
dir_path,
167+
normalized_pattern,
168+
should_ignore=lambda path, is_dir: self._is_gitignored(
169+
path, gitignore_spec, is_dir
170+
),
222171
)
223172
else:
224173
matches = [match async for match in dir_path.glob(params.pattern)]

0 commit comments

Comments
 (0)