Files
pi-auto-reinject/src/detect.ts
T
grayhook cc5ffc47bf Phase 3: add trackReadPaths gate — skip read-path detection when disabled.
Honor skillReinject.trackReadPaths before matching read tool paths to skills.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 10:24:49 +07:00

75 lines
2.1 KiB
TypeScript

import { isAbsolute, normalize, resolve } from "node:path";
import type { Skill } from "@earendil-works/pi-coding-agent";
/** Raw `/skill:name` at start of user input (SPEC §6.2 #1). */
const SLASH_SKILL_RE = /^\/skill:([a-z0-9-]+)/;
/** mirror agent-session `parseSkillBlock` — global scan (SPEC §6.2 #2). */
const SKILL_BLOCK_RE =
/<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>/g;
export interface ParsedSkillBlock {
name: string;
location: string;
content: string;
}
/** Minimal skill fields for read-path matching (SPEC §6.2 #3). */
export type SkillPathMeta = Pick<Skill, "name" | "filePath" | "baseDir">;
function normalizePathForCompare(filePath: string): string {
return normalize(filePath);
}
/** Returns skill name when text is a slash skill command, else null. */
export function detectSlashSkill(text: string): string | null {
const match = SLASH_SKILL_RE.exec(text);
return match?.[1] ?? null;
}
/** All skill blocks in message text (empty when none). */
export function parseSkillBlocksFromText(text: string): ParsedSkillBlock[] {
const blocks: ParsedSkillBlock[] = [];
for (const match of text.matchAll(SKILL_BLOCK_RE)) {
blocks.push({
name: match[1],
location: match[2],
content: match[3],
});
}
return blocks;
}
/** Match read tool path to a registered skill SKILL.md (SPEC §6.2 #3). */
export function matchReadPathToSkill(
path: string,
skills: readonly SkillPathMeta[],
): SkillPathMeta | null {
const normalizedPath = normalizePathForCompare(path);
for (const skill of skills) {
const skillPath = normalizePathForCompare(skill.filePath);
if (normalizedPath === skillPath) {
return skill;
}
if (!isAbsolute(path)) {
const resolvedFromBase = normalizePathForCompare(resolve(skill.baseDir, path));
if (resolvedFromBase === skillPath) {
return skill;
}
}
}
return null;
}
/** Read-path detection only when trackReadPaths is enabled (SPEC §6.2, §3). */
export function matchReadPathToSkillWhenEnabled(
path: string,
skills: readonly SkillPathMeta[],
trackReadPaths: boolean,
): SkillPathMeta | null {
if (!trackReadPaths) {
return null;
}
return matchReadPathToSkill(path, skills);
}