dist-mcp / mcp / skillStatus.js
dist-mcp / mcp / skillStatus.js
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { getProjectRoot } from "../core-bundle.mjs";
// Unsloth Studio and VS Code Copilot both read from ~/.agents/skills (see
// tool-description-skill-concept.md); only Bionic uses its own ~/.lmstudio/skills.
export function installedSkillRoot(preset, homeDir, skillName) {
const home = homeDir ?? os.homedir();
return preset.name === "bionic" ? path.join(home, ".lmstudio", "skills", skillName) : path.join(home, ".agents", "skills", skillName);
}
export function sha256File(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
/**
* Compares the installed SKILL.md (client-side) against the LOCAL plugin-directory cache's
* canonical SKILL.<preset>.md (skills/<skillName>/) by content hash. That local cache is expected
* to already be fresh — callers must run `ensureLocalSkillCache()` first (see skillSync.ts); this
* function itself never touches the network. Never throws: any read error (missing canonical
* file, permission issue, ...) is treated as "current" so a broken check can never nag the agent
* or block the actual tool call.
*
* `installScriptPath` always points at this repo's own `skills/analyse-image/install/` scripts,
* regardless of `skillName` — 'image-display' has no install/ scripts of its own (see
* made-for-bionic-core/skills/image-display/), it's installed piggyback by the same scripts that
* install 'analyse-image' (see those scripts' "Also install the shared 'image-display' skill" step).
*/
export function checkSkillStatus(preset, useSkills, skillName, options) {
if (!useSkills)
return { state: "disabled" };
const projectRoot = options?.projectRoot ?? getProjectRoot();
const skillDir = path.join(projectRoot, "skills", skillName);
const installScriptPath = path.join(projectRoot, "skills", "analyse-image", "install", `install_${preset.name}_skills.sh`);
const targetDir = installedSkillRoot(preset, options?.homeDir, skillName);
const installedSkillPath = path.join(targetDir, "SKILL.md");
try {
if (!fs.existsSync(installedSkillPath))
return { state: "not-installed", installScriptPath };
const canonicalSkillPath = path.join(skillDir, `SKILL.${preset.name}.md`);
const installedHash = sha256File(installedSkillPath);
if (sha256File(canonicalSkillPath) === installedHash)
return { state: "current" };
const syncedHashPath = path.join(targetDir, ".synced-hash");
const lastSyncedHash = fs.existsSync(syncedHashPath) ? fs.readFileSync(syncedHashPath, "utf-8").trim() : null;
return { state: "stale", installScriptPath, userModified: lastSyncedHash !== installedHash };
}
catch {
return { state: "current" };
}
}
/**
* Surfaces at most one notice per server process lifetime (one MCP server run ~= one client
* session) — repeating it on every tool call would be exactly the nagging behavior we want to
* avoid. "disabled" never produces a notice. "current" normally doesn't either — EXCEPT for
* 'image-display': real-world Bionic traces (2026-10-09) showed the agent never opens that skill
* on its own even when it's correctly installed — a passive "## Related skills" mention in
* SKILL.md's body is not enough to get it read. So 'image-display' gets a one-time reminder even
* when "current", pushing the agent to actually read it instead of assuming the mention sufficed.
*/
export function createSkillNoticeProvider(status, skillName) {
let shown = false;
return () => {
if (shown)
return undefined;
if (status.state === "disabled")
return undefined;
if (status.state === "current") {
if (skillName !== "image-display")
return undefined;
shown = true;
return "Note: before deciding how to present this result (browser vs. chat, what the user actually sees), read skill 'image-display' once this session if you haven't already — it's easy to miss since it's only referenced passively from SKILL.md's 'Related skills' section.";
}
shown = true;
if (status.state === "not-installed") {
return `Note: the '${skillName}' skill is not installed for this client yet. Ask the user whether to install it by running \`${status.installScriptPath}\`, or whether to set USE_SKILLS=false in this server's environment to stop seeing this note.`;
}
if (status.userModified) {
return `Note: the installed '${skillName}' skill was manually edited and differs from the current version. Your edits were left untouched. Ask the user to re-run \`${status.installScriptPath}\` if they want the update instead.`;
}
return `Note: the installed '${skillName}' skill is out of date. Ask the user to re-run \`${status.installScriptPath}\` to update it.`;
};
}
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { getProjectRoot } from "../core-bundle.mjs";
// Unsloth Studio and VS Code Copilot both read from ~/.agents/skills (see
// tool-description-skill-concept.md); only Bionic uses its own ~/.lmstudio/skills.
export function installedSkillRoot(preset, homeDir, skillName) {
const home = homeDir ?? os.homedir();
return preset.name === "bionic" ? path.join(home, ".lmstudio", "skills", skillName) : path.join(home, ".agents", "skills", skillName);
}
export function sha256File(filePath) {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
/**
* Compares the installed SKILL.md (client-side) against the LOCAL plugin-directory cache's
* canonical SKILL.<preset>.md (skills/<skillName>/) by content hash. That local cache is expected
* to already be fresh — callers must run `ensureLocalSkillCache()` first (see skillSync.ts); this
* function itself never touches the network. Never throws: any read error (missing canonical
* file, permission issue, ...) is treated as "current" so a broken check can never nag the agent
* or block the actual tool call.
*
* `installScriptPath` always points at this repo's own `skills/analyse-image/install/` scripts,
* regardless of `skillName` — 'image-display' has no install/ scripts of its own (see
* made-for-bionic-core/skills/image-display/), it's installed piggyback by the same scripts that
* install 'analyse-image' (see those scripts' "Also install the shared 'image-display' skill" step).
*/
export function checkSkillStatus(preset, useSkills, skillName, options) {
if (!useSkills)
return { state: "disabled" };
const projectRoot = options?.projectRoot ?? getProjectRoot();
const skillDir = path.join(projectRoot, "skills", skillName);
const installScriptPath = path.join(projectRoot, "skills", "analyse-image", "install", `install_${preset.name}_skills.sh`);
const targetDir = installedSkillRoot(preset, options?.homeDir, skillName);
const installedSkillPath = path.join(targetDir, "SKILL.md");
try {
if (!fs.existsSync(installedSkillPath))
return { state: "not-installed", installScriptPath };
const canonicalSkillPath = path.join(skillDir, `SKILL.${preset.name}.md`);
const installedHash = sha256File(installedSkillPath);
if (sha256File(canonicalSkillPath) === installedHash)
return { state: "current" };
const syncedHashPath = path.join(targetDir, ".synced-hash");
const lastSyncedHash = fs.existsSync(syncedHashPath) ? fs.readFileSync(syncedHashPath, "utf-8").trim() : null;
return { state: "stale", installScriptPath, userModified: lastSyncedHash !== installedHash };
}
catch {
return { state: "current" };
}
}
/**
* Surfaces at most one notice per server process lifetime (one MCP server run ~= one client
* session) — repeating it on every tool call would be exactly the nagging behavior we want to
* avoid. "disabled" never produces a notice. "current" normally doesn't either — EXCEPT for
* 'image-display': real-world Bionic traces (2026-10-09) showed the agent never opens that skill
* on its own even when it's correctly installed — a passive "## Related skills" mention in
* SKILL.md's body is not enough to get it read. So 'image-display' gets a one-time reminder even
* when "current", pushing the agent to actually read it instead of assuming the mention sufficed.
*/
export function createSkillNoticeProvider(status, skillName) {
let shown = false;
return () => {
if (shown)
return undefined;
if (status.state === "disabled")
return undefined;
if (status.state === "current") {
if (skillName !== "image-display")
return undefined;
shown = true;
return "Note: before deciding how to present this result (browser vs. chat, what the user actually sees), read skill 'image-display' once this session if you haven't already — it's easy to miss since it's only referenced passively from SKILL.md's 'Related skills' section.";
}
shown = true;
if (status.state === "not-installed") {
return `Note: the '${skillName}' skill is not installed for this client yet. Ask the user whether to install it by running \`${status.installScriptPath}\`, or whether to set USE_SKILLS=false in this server's environment to stop seeing this note.`;
}
if (status.userModified) {
return `Note: the installed '${skillName}' skill was manually edited and differs from the current version. Your edits were left untouched. Ask the user to re-run \`${status.installScriptPath}\` if they want the update instead.`;
}
return `Note: the installed '${skillName}' skill is out of date. Ask the user to re-run \`${status.installScriptPath}\` to update it.`;
};
}