src / types.ts
import * as fs from "fs/promises";
/** One skill folder: a directory under the skills root containing SKILL.md. */
export interface Skill {
/** Frontmatter `name`, falling back to the folder name. */
name: string;
/** Frontmatter `description`; empty string when missing or unreadable. */
description: string;
/** Absolute path of the skill directory. */
dir: string;
/** Absolute path of SKILL.md. */
skillMdPath: string;
/** Eligible for jev auto-routing: has a description, not disabled, not a duplicate. */
routable: boolean;
/** Frontmatter `disable-model-invocation: true`. Still explicitly activatable. */
disableModelInvocation: boolean;
/** Human-readable problem note shown in list_skills (e.g. "frontmatter unreadable"). */
note?: string;
}
export interface SkillIndex {
root: string;
skills: Skill[];
byName: Map<string, Skill>;
/** Skills eligible for jev routing, capped at MAX_ROUTABLE, most recently modified first. */
routable: Skill[];
}
/** Per-chat settings, read from configSchematics. */
export interface Settings {
threshold: number;
maxSkills: number;
historyTurns: number;
maxInjectedChars: number;
showHintWhenNoMatch: boolean;
}
export interface Turn {
role: string;
text: string;
}
export interface RouteInput {
message: string;
recentTurns: Turn[];
}
/** Returns a probability (0-1) per routable skill name that the skill applies. */
export type Judge = (input: RouteInput, skills: Skill[], signal?: AbortSignal) => Promise<Record<string, number>>;
export interface DirEntry {
name: string;
isDirectory: boolean;
isFile: boolean;
}
export interface FileStat {
mtimeMs: number;
size: number;
isDirectory: boolean;
isFile: boolean;
}
/** Minimal filesystem surface used by the plugin, so tests can supply an in-memory one. */
export interface SkillFs {
readdir(dir: string): Promise<DirEntry[]>;
stat(path: string): Promise<FileStat>;
readFile(path: string): Promise<Buffer>;
realpath(path: string): Promise<string>;
}
export const nodeFs: SkillFs = {
async readdir(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
return entries.map(e => ({ name: e.name, isDirectory: e.isDirectory(), isFile: e.isFile() }));
},
async stat(path) {
const s = await fs.stat(path);
return { mtimeMs: s.mtimeMs, size: s.size, isDirectory: s.isDirectory(), isFile: s.isFile() };
},
readFile: path => fs.readFile(path),
realpath: path => fs.realpath(path),
};
export const JEV_TIMEOUT_MS = 6000;
export const TOOL_READ_CAP = 40_000;
export const MAX_ROUTABLE = 40;
export const HISTORY_TEXT_CAP = 500;
export const TREE_MAX_DEPTH = 4;
src / types.ts
import * as fs from "fs/promises";
/** One skill folder: a directory under the skills root containing SKILL.md. */
export interface Skill {
/** Frontmatter `name`, falling back to the folder name. */
name: string;
/** Frontmatter `description`; empty string when missing or unreadable. */
description: string;
/** Absolute path of the skill directory. */
dir: string;
/** Absolute path of SKILL.md. */
skillMdPath: string;
/** Eligible for jev auto-routing: has a description, not disabled, not a duplicate. */
routable: boolean;
/** Frontmatter `disable-model-invocation: true`. Still explicitly activatable. */
disableModelInvocation: boolean;
/** Human-readable problem note shown in list_skills (e.g. "frontmatter unreadable"). */
note?: string;
}
export interface SkillIndex {
root: string;
skills: Skill[];
byName: Map<string, Skill>;
/** Skills eligible for jev routing, capped at MAX_ROUTABLE, most recently modified first. */
routable: Skill[];
}
/** Per-chat settings, read from configSchematics. */
export interface Settings {
threshold: number;
maxSkills: number;
historyTurns: number;
maxInjectedChars: number;
showHintWhenNoMatch: boolean;
}
export interface Turn {
role: string;
text: string;
}
export interface RouteInput {
message: string;
recentTurns: Turn[];
}
/** Returns a probability (0-1) per routable skill name that the skill applies. */
export type Judge = (input: RouteInput, skills: Skill[], signal?: AbortSignal) => Promise<Record<string, number>>;
export interface DirEntry {
name: string;
isDirectory: boolean;
isFile: boolean;
}
export interface FileStat {
mtimeMs: number;
size: number;
isDirectory: boolean;
isFile: boolean;
}
/** Minimal filesystem surface used by the plugin, so tests can supply an in-memory one. */
export interface SkillFs {
readdir(dir: string): Promise<DirEntry[]>;
stat(path: string): Promise<FileStat>;
readFile(path: string): Promise<Buffer>;
realpath(path: string): Promise<string>;
}
export const nodeFs: SkillFs = {
async readdir(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
return entries.map(e => ({ name: e.name, isDirectory: e.isDirectory(), isFile: e.isFile() }));
},
async stat(path) {
const s = await fs.stat(path);
return { mtimeMs: s.mtimeMs, size: s.size, isDirectory: s.isDirectory(), isFile: s.isFile() };
},
readFile: path => fs.readFile(path),
realpath: path => fs.realpath(path),
};
export const JEV_TIMEOUT_MS = 6000;
export const TOOL_READ_CAP = 40_000;
export const MAX_ROUTABLE = 40;
export const HISTORY_TEXT_CAP = 500;
export const TREE_MAX_DEPTH = 4;