2021ee1293
Scan finalized user message text for expanded skill XML blocks and upsert tracked skills using registered metadata when available. Co-authored-by: Cursor <cursoragent@cursor.com>
96 lines
2.7 KiB
TypeScript
96 lines
2.7 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);
|
|
}
|
|
|
|
/** Text from user message content (string or text blocks). */
|
|
export function userMessageText(content: unknown): string {
|
|
if (typeof content === "string") {
|
|
return content;
|
|
}
|
|
if (!Array.isArray(content)) {
|
|
return "";
|
|
}
|
|
const parts: string[] = [];
|
|
for (const part of content) {
|
|
if (!part || typeof part !== "object") {
|
|
continue;
|
|
}
|
|
const block = part as { type?: string; text?: string };
|
|
if (block.type === "text" && typeof block.text === "string") {
|
|
parts.push(block.text);
|
|
}
|
|
}
|
|
return parts.join("\n");
|
|
}
|
|
|
|
/** 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);
|
|
}
|