Project Files
src / preprocessor.ts
import { resolveEffectiveConfig } from "./settings";
import { scanSkills } from "./scanner";
import { MIN_PROMPT_LENGTH, REINJECT_INTERVAL_MS } from "./constants";
import type { PluginController } from "./pluginTypes";
import type { SkillInfo } from "./types";
function buildAvailableSkillsBlock(skills: SkillInfo[], limit: number): string {
const skillTags = skills
.slice(0, limit)
.map((s) =>
[
`<skill>`,
`<n>`,
s.name,
`</n>`,
`<description>`,
s.description,
`</description>`,
`<location>`,
s.skillMdPath,
`</location>`,
`</skill>`,
].join("\n"),
)
.join("\n\n");
return `<available_skills>\n${skillTags}\n</available_skills>`;
}
function buildInstruction(): string {
return "You have access to a set of skills listed in <available_skills>. Each skill is a directory containing a SKILL.md file with instructions and best practices built from real trial and error. Before starting any task that matches a skill, call `read_skill_file` with the skill name or its location path to load its instructions - always do this before writing any code, creating files, or producing output the skill covers. Multiple skills may be relevant to a single task; read all of them before proceeding, do not limit yourself to one. After reading SKILL.md, if it references additional files, call `list_skill_files` to discover them, then read whichever ones apply. Use `list_skills` with a query to search for relevant skills by name and description when the task does not match anything in the list above - not all installed skills may be shown here.";
}
function buildInjection(skills: SkillInfo[], limit: number): string {
return [
buildInstruction(),
"",
buildAvailableSkillsBlock(skills, limit),
].join("\n");
}
function computeFingerprint(skills: SkillInfo[]): string {
return skills
.map((s) => s.skillMdPath)
.sort()
.join("|");
}
let lastFingerprint = "";
let lastInjectedAt = 0;
type MessageContent =
| { type: "text"; text: string }
| { type: string; [key: string]: unknown };
type MessageInput = string | { content: string | MessageContent[] } | unknown;
function extractText(message: MessageInput): string {
if (typeof message === "string") return message;
if (message !== null && typeof message === "object") {
const m = message as Record<string, unknown>;
if (typeof m.content === "string") return m.content;
if (Array.isArray(m.content)) {
return m.content
.filter(
(c): c is MessageContent =>
typeof c === "object" &&
c !== null &&
(c as MessageContent).type === "text",
)
.map((c) => (c as { type: "text"; text: string }).text)
.join("");
}
if (typeof m.text === "string") return m.text;
}
return String(message ?? "");
}
function injectIntoMessage(
message: MessageInput,
injection: string,
): MessageInput {
if (typeof message === "string") {
return `${injection}\n\n---\n\n${message}`;
}
if (message !== null && typeof message === "object") {
const m = message as Record<string, unknown>;
if (typeof m.content === "string") {
return { ...m, content: `${injection}\n\n---\n\n${m.content}` };
}
if (Array.isArray(m.content)) {
const first = m.content.findIndex(
(c) =>
typeof c === "object" &&
c !== null &&
(c as MessageContent).type === "text",
);
if (first !== -1) {
const updated = [...m.content] as MessageContent[];
const block = updated[first] as { type: "text"; text: string };
updated[first] = {
...block,
text: `${injection}\n\n---\n\n${block.text}`,
};
return { ...m, content: updated };
}
return {
...m,
content: [{ type: "text", text: injection }, ...m.content],
};
}
}
return message;
}
export async function promptPreprocessor(
ctl: PluginController,
userMessage: MessageInput,
): Promise<MessageInput> {
const cfg = resolveEffectiveConfig(ctl);
if (!cfg.autoInject) return userMessage;
const text = extractText(userMessage);
if (text.trim().length < MIN_PROMPT_LENGTH) return userMessage;
try {
const skills = scanSkills(cfg.skillsPaths);
if (skills.length === 0) return userMessage;
const fingerprint = computeFingerprint(skills);
const now = Date.now();
const skillsChanged = fingerprint !== lastFingerprint;
const intervalElapsed = now - lastInjectedAt > REINJECT_INTERVAL_MS;
if (!skillsChanged && !intervalElapsed) return userMessage;
lastFingerprint = fingerprint;
lastInjectedAt = now;
return injectIntoMessage(
userMessage,
buildInjection(skills, cfg.maxSkillsInContext),
);
} catch {
return userMessage;
}
}