@tanstack/ai-skills
Version:
Portable Agent Skills (SKILL.md) as a first-class chat() middleware for TanStack AI.
85 lines (77 loc) • 2.64 kB
text/typescript
/**
* Generic skill-directory walk, shared with `@tanstack/ai-sandbox`.
*
* The algorithm is identical to the one in `ai-sandbox/src/agents-file.ts`, but
* parameterized over an injected `list` function so it works over any backing
* store (`node:fs`, a `SandboxHandle.fs`, an in-memory tree). Taking only an
* injected function keeps this edge-safe, so it lives in the root barrel.
*/
/** A directory that contains `SKILL.md`. */
export interface DiscoveredSkillDir {
name: string
dir: string
}
/** One entry as reported by an injected {@link ListDir}. */
export interface WalkEntry {
name: string
path: string
type: 'file' | 'dir'
}
export type ListDir = (dir: string) => Promise<Array<WalkEntry>>
export const SKILL_FILE = 'SKILL.md'
export const MAX_SKILL_WALK_DEPTH = 6
const SKIP_DIR_NAMES = new Set(['.git', 'node_modules'])
function basenameOf(path: string): string {
const normalized = path.replace(/\\/g, '/')
const segments = normalized.split('/').filter((segment) => segment !== '')
return segments[segments.length - 1] ?? path
}
/**
* Find every skill folder under `root`. A skill folder is a directory that
* directly contains `SKILL.md`; the walk stops descending once found. Skips
* dot-directories, `.git`, and `node_modules`. Bounded by `maxDepth`. Errors
* from `list` are swallowed (an unreadable directory yields nothing).
*
* Unlike `ai-sandbox`'s `discoverSkillDirs`, this returns `[]` when nothing is
* found — the "fall back to the clone dir" behavior is a harness-projection
* concern and stays at that call site (it is wrong for a catalog).
*/
export async function walkSkillDirs(
list: ListDir,
root: string,
opts: { maxDepth?: number } = {},
): Promise<Array<DiscoveredSkillDir>> {
const maxDepth = opts.maxDepth ?? MAX_SKILL_WALK_DEPTH
const found: Array<DiscoveredSkillDir> = []
await walk(list, root, found, 0, maxDepth)
return found
}
async function walk(
list: ListDir,
dir: string,
found: Array<DiscoveredSkillDir>,
depth: number,
maxDepth: number,
): Promise<void> {
if (depth > maxDepth) return
let entries: Array<WalkEntry>
try {
entries = await list(dir)
} catch {
return
}
const hasSkill = entries.some(
(entry) =>
entry.type === 'file' &&
entry.name.toLowerCase() === SKILL_FILE.toLowerCase(),
)
if (hasSkill) {
found.push({ name: basenameOf(dir), dir })
return
}
for (const entry of entries) {
if (entry.type !== 'dir') continue
if (entry.name.startsWith('.') || SKIP_DIR_NAMES.has(entry.name)) continue
await walk(list, entry.path, found, depth + 1, maxDepth)
}
}