From f89b49baeb73500b01dbe26aa033f7ae73f84443 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 5 Feb 2026 20:24:15 +0100 Subject: [PATCH] fix(coding-agent): respect ignore files in skill loader --- packages/ai/CHANGELOG.md | 4 ++ packages/coding-agent/CHANGELOG.md | 11 ++++ packages/coding-agent/src/core/skills.ts | 74 +++++++++++++++++++++++- 3 files changed, 86 insertions(+), 3 deletions(-) diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 9c8f4385..cd869ace 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added Claude Opus 4.6 model to the generated model catalog + ## [0.51.6] - 2026-02-04 ### Fixed diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ca10fbde..e21b14f5 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,10 +2,19 @@ ## [Unreleased] +### New Features + +- SSH URL support for git packages. See [docs/packages.md](docs/packages.md). +- `auth.json` API keys now support shell command resolution (`!command`) and environment variable lookup. See [docs/providers.md](docs/providers.md). +- Model selectors now display the selected model name. + ### Added - API keys in `auth.json` now support shell command resolution (`!command`) and environment variable lookup, matching the behavior in `models.json` - Added `minimal-mode.ts` example extension demonstrating how to override built-in tool rendering for a minimal display mode +- Added Claude Opus 4.6 model to the model catalog +- Added SSH URL support for git packages ([#1287](https://github.com/badlogic/pi-mono/pull/1287) by [@markusn](https://github.com/markusn)) +- Model selectors now display the selected model name ([#1275](https://github.com/badlogic/pi-mono/pull/1275) by [@haoqixu](https://github.com/haoqixu)) ### Fixed @@ -13,6 +22,8 @@ - Fixed images being silently dropped when `prompt()` is called with both `images` and `streamingBehavior` during streaming. `steer()`, `followUp()`, and the corresponding RPC commands now accept optional images. ([#1271](https://github.com/badlogic/pi-mono/pull/1271) by [@aliou](https://github.com/aliou)) - CLI `--help`, `--version`, `--list-models`, and `--export` now exit even if extensions keep the event loop alive ([#1285](https://github.com/badlogic/pi-mono/pull/1285) by [@ferologics](https://github.com/ferologics)) - Fixed crash when models send malformed tool arguments (objects instead of strings) ([#1259](https://github.com/badlogic/pi-mono/issues/1259)) +- Fixed custom message expand state not being respected ([#1258](https://github.com/badlogic/pi-mono/pull/1258) by [@Gurpartap](https://github.com/Gurpartap)) +- Fixed skill loader to respect .gitignore, .ignore, and .fdignore when scanning directories ## [0.51.6] - 2026-02-04 diff --git a/packages/coding-agent/src/core/skills.ts b/packages/coding-agent/src/core/skills.ts index 2666d04b..70e1aa64 100644 --- a/packages/coding-agent/src/core/skills.ts +++ b/packages/coding-agent/src/core/skills.ts @@ -1,6 +1,7 @@ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "fs"; +import ignore from "ignore"; import { homedir } from "os"; -import { basename, dirname, isAbsolute, join, resolve, sep } from "path"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path"; import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; import { parseFrontmatter } from "../utils/frontmatter.js"; import type { ResourceDiagnostic } from "./diagnostics.js"; @@ -11,6 +12,57 @@ const MAX_NAME_LENGTH = 64; /** Max description length per spec */ const MAX_DESCRIPTION_LENGTH = 1024; +const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"]; + +type IgnoreMatcher = ReturnType; + +function toPosixPath(p: string): string { + return p.split(sep).join("/"); +} + +function prefixIgnorePattern(line: string, prefix: string): string | null { + const trimmed = line.trim(); + if (!trimmed) return null; + if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) return null; + + let pattern = line; + let negated = false; + + if (pattern.startsWith("!")) { + negated = true; + pattern = pattern.slice(1); + } else if (pattern.startsWith("\\!")) { + pattern = pattern.slice(1); + } + + if (pattern.startsWith("/")) { + pattern = pattern.slice(1); + } + + const prefixed = prefix ? `${prefix}${pattern}` : pattern; + return negated ? `!${prefixed}` : prefixed; +} + +function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void { + const relativeDir = relative(rootDir, dir); + const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : ""; + + for (const filename of IGNORE_FILE_NAMES) { + const ignorePath = join(dir, filename); + if (!existsSync(ignorePath)) continue; + try { + const content = readFileSync(ignorePath, "utf-8"); + const patterns = content + .split(/\r?\n/) + .map((line) => prefixIgnorePattern(line, prefix)) + .filter((line): line is string => Boolean(line)); + if (patterns.length > 0) { + ig.add(patterns); + } + } catch {} + } +} + export interface SkillFrontmatter { name?: string; description?: string; @@ -96,7 +148,13 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills return loadSkillsFromDirInternal(dir, source, true); } -function loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles: boolean): LoadSkillsResult { +function loadSkillsFromDirInternal( + dir: string, + source: string, + includeRootFiles: boolean, + ignoreMatcher?: IgnoreMatcher, + rootDir?: string, +): LoadSkillsResult { const skills: Skill[] = []; const diagnostics: ResourceDiagnostic[] = []; @@ -104,6 +162,10 @@ function loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles return { skills, diagnostics }; } + const root = rootDir ?? dir; + const ig = ignoreMatcher ?? ignore(); + addIgnoreRules(ig, dir, root); + try { const entries = readdirSync(dir, { withFileTypes: true }); @@ -133,8 +195,14 @@ function loadSkillsFromDirInternal(dir: string, source: string, includeRootFiles } } + const relPath = toPosixPath(relative(root, fullPath)); + const ignorePath = isDirectory ? `${relPath}/` : relPath; + if (ig.ignores(ignorePath)) { + continue; + } + if (isDirectory) { - const subResult = loadSkillsFromDirInternal(fullPath, source, false); + const subResult = loadSkillsFromDirInternal(fullPath, source, false, ig, root); skills.push(...subResult.skills); diagnostics.push(...subResult.diagnostics); continue;