Project Files
src / toolsProvider.ts
import { text, tool, type ToolsProviderController } from "@lmstudio/sdk";
import { z } from "zod";
import { getConfigSchematics } from "./configSchematics";
import { getActiveServer, startWebServer } from "./web";
import { listSessions } from "./storage";
export async function toolsProvider(ctl: ToolsProviderController) {
// getConfigSchematics() retourne la version rebuilt avec la liste de
// modèles fetchée à l'init du plugin (cf. index.ts main()).
const config = ctl.getPluginConfig(getConfigSchematics());
function cfg() {
const mode = (config.get("mode") || "remote").trim().toLowerCase();
const isLocal = mode === "local";
const host = (isLocal ? config.get("localHost") : config.get("remoteHost"))?.trim()
|| (isLocal ? "localhost:1234" : "192.0.2.1:1234");
const model = (isLocal ? config.get("localModel") : config.get("remoteModel"))?.trim()
|| "qwen3-14b";
return {
host,
model,
temperature: config.get("temperature"),
maxRetries: config.get("maxRetries"),
mode: isLocal ? "local" : "remote",
};
}
const launchWebTool = tool({
name: "launch_livre_heros_web",
description: text`
Démarre un serveur HTTP local qui sert une page chat dans le
navigateur — destiné aux testeurs qui n'ont pas accès à LM Studio
ni à un terminal. Le navigateur s'ouvre automatiquement. Par
défaut le serveur écoute uniquement sur 127.0.0.1 ; passer
bind="lan" pour l'exposer sur le réseau local (sans auth — Ã
réserver aux réseaux de confiance).
Chaque navigateur ouvre sa propre session (UUID en localStorage),
persistée sur disque dans ~/.livre-heros-bac/sessions/. Les sessions
survivent aux rebuilds esbuild et aux fermetures de l'app : un
testeur qui rafraîchit son onglet retrouve sa partie en cours.
Le serveur s'auto-arrête après 30 min sans requête HTTP.
USAGE : "lance la démo dans le navigateur", "démarre le serveur web
livre-héros". Tu peux passer notion/sujet pour pré-régler les
défauts, mais chaque session est libre de partir sur autre chose
via son propre /start.
`,
parameters: {
// Important : ne pas écrire « Défaut : config. » ici — les LLM lisent ça
// comme « la valeur par défaut est la chaîne "config" » et passent
// littéralement notion="config", ce qui filtre le corpus à vide
// (bug observé sur la session 5be2aa…, 2026-05-18).
notion: z.string().optional().describe("Slug de notion BO (liberte, justice, conscience…). Optionnel — si omis, la valeur est lue dans les paramètres du plugin (defaultNotion)."),
sujet: z.string().optional().describe("Énoncé de dissertation. Optionnel — si omis, la valeur est lue dans les paramètres du plugin (defaultSujet)."),
port: z.number().int().min(0).max(65535).optional().describe("Port d'écoute. Défaut : 7321."),
bind: z.enum(["lan", "local"]).optional().describe("'lan' (0.0.0.0, accessible depuis tout le réseau local — pour testeurs sur d'autres machines) ou 'local' (127.0.0.1, ce poste uniquement — recommandé). Défaut : 'local'."),
},
implementation: async ({ notion, sujet, port, bind }) => {
const c = cfg();
const defaultNotion = (notion ?? config.get("defaultNotion") ?? "liberte").trim().toLowerCase();
const defaultSujet = sujet ?? config.get("defaultSujet") ?? "La liberté est-elle l'absence de contrainte ?";
// S2 — défaut sur 127.0.0.1 : il faut passer bind="lan" explicitement
// pour exposer le serveur sur le réseau. Avant, le défaut 0.0.0.0
// ouvrait /admin et /api/corpus/* Ã tout le LAN sans auth.
const bindHost = bind === "lan" ? "0.0.0.0" : "127.0.0.1";
const storageDir = config.get("storageDir")?.trim() || undefined;
// Bypass : si l'utilisateur a passé un sujet explicite, on ouvre direct
// sur /play avec ces params (raccourci pour les itérations dev).
// Sinon, on ouvre sur le catalogue où le parent choisit/ajoute.
const openPath = sujet
? `/play?notion=${encodeURIComponent(defaultNotion)}&sujet=${encodeURIComponent(defaultSujet)}`
: "/";
const handle = await startWebServer({
defaultNotion,
defaultSujet,
cfg: c,
bindHost,
port: port ?? 7321,
openBrowser: true,
openPath,
storageDir,
});
return {
url_locale: handle.url,
urls_lan: handle.lanUrls,
modele: `${c.mode} · ${c.host} · ${c.model}`,
notion_par_defaut: defaultNotion,
sujet_par_defaut: defaultSujet,
mode_demarrage: sujet ? "bypass direct vers /play" : "catalogue de sujets",
message: sujet
? "Navigateur ouvert directement sur la page de jeu (bypass catalogue car sujet fourni)."
: "Navigateur ouvert sur le catalogue de sujets. Les parents peuvent choisir ou ajouter un sujet.",
};
},
});
const stopWebTool = tool({
name: "stop_livre_heros_web",
description: text`Arrête le serveur web livre-héros s'il tourne (n'efface pas les sessions sur disque).`,
parameters: {},
implementation: async () => {
const s = getActiveServer();
if (!s) return { stopped: false, message: "Aucun serveur actif." };
await s.close();
return { stopped: true };
},
});
const listSessionsTool = tool({
name: "list_livre_heros_sessions",
description: text`
Liste les sessions actives ou récentes (moins de 24h) dans
~/.livre-heros-bac/sessions/. Utile pour voir ce que les testeurs
ont joué pendant la démo.
`,
parameters: {},
implementation: async () => {
return { sessions: listSessions() };
},
});
return [launchWebTool, stopWebTool, listSessionsTool];
}