feat: give assistants each page as markdown

The build writes <page>/index.md beside each page, named in its head,
and llms-full.txt. llms.txt and the setup skill say how to answer from
the site, not from memory, and the setup_bios_files WebMCP tool gives a
setup's folder and exact file names from the picker data.
This commit is contained in:
Abdessamad Derraz committed 2026-11-09 06:04:42 +01:00
1 parent ab9fe76de2
commit a7a27028ab
8 files changed
+493 -2

No files matched your search

+63 -1
View File
@@ -117,6 +117,68 @@ const searchSite: Tool = {
},
};
// What a setup reads, from the data the file picker reads
// (data/picker/<setup>.json, written by scripts/generate_site.py): the BIOS
// folder and, per system, each file's exact name and size. An assistant
// that answers from it names no file and no folder the data does not hold.
type PickerIndex = { platform: string; name: string; files: number }[];
type PickerSystem = { id: string; title: string; native: string[] };
type PickerData = { platform: string; name: string; folder: string; systems: PickerSystem[]; files: [string, number, number[]][] };
const words = (text: string) => text.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
const setupFiles: Tool = {
name: 'setup_bios_files',
description:
'The BIOS folder of a setup (RetroArch, Batocera, EmuDeck, RetroBat, Recalbox, KNULLI, ROCKNIX, AmberELEC, MiSTer and others) '
+ 'and the exact file names it reads for a system, from the site\'s data. Use it instead of guessing a folder or a file name: names '
+ 'are case-sensitive and must not be renamed.',
inputSchema: {
type: 'object',
properties: {
setup: { type: 'string', description: 'The setup, for example "batocera", "RetroArch" or "EmuDeck".' },
system: { type: 'string', description: 'A system, for example "PlayStation 2", "psx" or "gba". Without it, the systems are listed.' },
},
required: ['setup'],
},
annotations: { readOnlyHint: true },
async execute({ setup, system }) {
const asked = words(String(setup ?? ''));
const index = await json<PickerIndex>('data/picker/index.json');
const found = index.find((item) => item.platform === asked.replace(/ /g, '') || words(item.name) === asked);
if (!found) return `No setup named "${String(setup)}". Setups: ${index.map((item) => item.name).join(', ')}.`;
const data = await json<PickerData>(`data/picker/${found.platform}.json`);
const page = absolute(`${BASE}platforms/${found.platform}/`);
// RetroArch and EmuDeck keep no single folder: it depends on the device.
const folder = data.folder || `Depends on the device and its system: ${page}#where-the-files-go`;
const wanted = words(String(system ?? ''));
if (!wanted) {
return JSON.stringify({ setup: data.name, bios_folder: folder, page,
systems: data.systems.map((item) => ({ id: item.id, title: item.title })) }, null, 1);
}
const matches = data.systems
.map((item, position) => ({ item, position }))
.filter(({ item }) => [item.id, item.title, ...item.native].some((name) => {
const said = words(name);
return said === wanted || said.split(' ').includes(wanted) || said.includes(wanted);
}));
if (!matches.length) return `${data.name} lists no system matching "${String(system)}". Call again without system to list them.`;
const positions = new Set(matches.map(({ position }) => position));
const files = data.files.filter(([, , systems]) => systems.some((position) => positions.has(position)));
return JSON.stringify({
setup: data.name,
bios_folder: folder,
systems: matches.map(({ item }) => item.title),
note: 'Every file the emulators of this setup read for these systems: an emulator reads only some of them '
+ '(its page names them, emulator_bios_files lists them). Keep each name exactly as written.',
files: files.slice(0, 200).map(([name, bytes]) => ({ name, bytes })),
more: Math.max(files.length - 200, 0),
page,
install: 'device_setup_command gives the command that installs and checks these files.',
}, null, 1);
},
};
// The command an agent runs to set up its user's device, from the same
// options wiki/installer.md documents: the fewest files the games on the
// card need, unless the user asks for everything.
@@ -177,7 +239,7 @@ const setupCommand: Tool = {
const context = (document as Document & { modelContext?: ModelContext }).modelContext;
if (context?.registerTool) {
for (const tool of [setupCommand, lookupHash, emulatorFiles, searchSite]) {
for (const tool of [setupCommand, setupFiles, lookupHash, emulatorFiles, searchSite]) {
Promise.resolve(context.registerTool(tool)).catch((error) => console.warn(`WebMCP: ${tool.name}:`, error));
}
}