diff --git a/README.md b/README.md index 558651c..800bb3b 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ The site of the [missing-elements](https://github.com/missing-elements) web comp | `/h5p/build/` | The build page, for people: what it makes, a sample that plays, and the build of a chat's source into a `.h5p`, in the visitor's browser | [src/h5p/build/](src/h5p/build/) | | `/h5p/create-with-ai/` | The how-to, for people: installing the skill, asking a chat, the prompts to start from, the types, and what to do when something goes wrong | [src/h5p/create-with-ai/](src/h5p/create-with-ai/) | | `/h5p/skill/` | The skill as zips to install: `h5p-author.zip` for Claude and Grok, `h5p-author-chatgpt.zip` for ChatGPT's Plugins; and its files, with a page that lists each at its full address, which no page links to | [src/h5p/skill/](src/h5p/skill/), copied by [eleventy.config.js](eleventy.config.js); the zips written by [vite.config.ts](vite.config.ts) | -| `/h5p/bundles/` | The skill's build tool and a library bundle per content type, for a chat that runs Node but cannot reach npm | written by [vite.config.ts](vite.config.ts) | +| `/h5p/bundles/` | The skill's build tool and a library bundle per content type, for a chat that runs Node but cannot reach npm; and `h5p-author-build.tar.gz`, the tool and every library in one archive, for a person to attach to a chat whose code can download nothing | written by [vite.config.ts](vite.config.ts) | | `/h5p/build/types/` | The skill's type files, which the build page copies for the chat | written by [vite.config.ts](vite.config.ts) | -A chat takes the skill installed: from a zip in Claude, ChatGPT and Grok, or with `npx skills add` in an agent with a terminal, as `/h5p/create-with-ai/` says. Where the skill cannot reach npm, it builds from the tool and a bundle in `/h5p/bundles/`, at the addresses `/h5p/skill/` lists, if it can run Node and fetch a file: Grok did, on 2026-11-13. If it cannot, as in ChatGPT, it gives the user a link to `/h5p/build/`, which builds in their browser. +A chat takes the skill installed: from a zip in Claude, ChatGPT and Grok, or with `npx skills add` in an agent with a terminal, as `/h5p/create-with-ai/` says. Where the skill cannot reach npm, it builds from the tool and a bundle in `/h5p/bundles/`, at the addresses `/h5p/skill/` lists, if it can run Node and fetch a file: Grok did, on 2026-11-13. Where its code runs Node but can fetch nothing, as in ChatGPT, it builds from `h5p-author-build.tar.gz` once the person attaches it, and otherwise gives them a link to `/h5p/build/`, which builds in their browser. The tool is a Node.js program, so a chat whose code runs no Node, as most chat apps for local models, takes the link; an agent with a terminal and a local model builds offline from the archive. Until 2026-11-13 the site's prompts sent a chat to read the skill at `/h5p/skill/` instead. That worked only in chats that now take the zip, so the prompts name the installed skill, and no page links to `/h5p/skill/`. Its page and files stay: the copies of the skill already installed name that address, and so do the prompts people copied before. diff --git a/package.json b/package.json index 9deb7aa..5c3facd 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "@codemirror/state": "6.7.6", "@codemirror/view": "6.43.15", "@lezer/highlight": "1.2.5", - "@missing-elements/h5p-author": "0.9.7", + "@missing-elements/h5p-author": "0.9.8", "@missing-elements/h5p-libraries": "1.1.0", "@missing-elements/h5p-offline-player": "1.6.0", "@missing-elements/h5p-runtime": "1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4ef038..4591457 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: 1.2.5 version: 1.2.5 '@missing-elements/h5p-author': - specifier: 0.9.7 - version: 0.9.7(@missing-elements/h5p-runtime@1.0.1) + specifier: 0.9.8 + version: 0.9.8(@missing-elements/h5p-runtime@1.0.1) '@missing-elements/h5p-libraries': specifier: 1.1.0 version: 1.1.0 @@ -155,8 +155,8 @@ packages: '@marijn/find-cluster-break@1.0.4': resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} - '@missing-elements/h5p-author@0.9.7': - resolution: {integrity: sha512-9QQCKf6902Mukrofo+kQtekPQ4waWtqLjt+lnYFv8bvFErmH0tcE12h3tt9N+e4ryKlOPcg3t8HsyCyRBwgFsw==} + '@missing-elements/h5p-author@0.9.8': + resolution: {integrity: sha512-w98Cii8n5/46skRj6rXbbdTSIJrpcli2550ZTkQ9Q0m8/nK/3+hK9uDbldM6B56el6Mc/FI1ncByntmD26YFpw==} engines: {node: '>=22'} hasBin: true @@ -1247,7 +1247,7 @@ snapshots: '@marijn/find-cluster-break@1.0.4': {} - '@missing-elements/h5p-author@0.9.7(@missing-elements/h5p-runtime@1.0.1)': + '@missing-elements/h5p-author@0.9.8(@missing-elements/h5p-runtime@1.0.1)': dependencies: '@missing-elements/h5p-libraries': 1.1.0 '@missing-elements/h5p-normalize': 1.3.0 diff --git a/src/h5p/build/index.njk b/src/h5p/build/index.njk index f19fd91..2907994 100644 --- a/src/h5p/build/index.njk +++ b/src/h5p/build/index.njk @@ -1,10 +1,11 @@ --- layout: base.njk # The title and description carry what people search for: an AI H5P generator, with ChatGPT and the -# like. The description counts the types as the hero does, in the release the page runs. -title: "Free AI H5P generator: make H5P with ChatGPT, Claude or Grok" +# like, and what sets it apart, in the owner's words: privacy first, no MCP server. The description +# counts the types as the hero does, in the release the page runs. +title: "Free AI H5P generator, privacy first: make H5P with ChatGPT, Claude or Grok" eleventyComputed: - description: "Describe a lesson to ChatGPT, Claude or Grok and get an H5P activity: a quiz, a course presentation or any of {{ skill.offered.length }} H5P types. Free, no account, for Moodle." + description: "Describe a lesson to ChatGPT, Claude or Grok and get an H5P activity for Moodle: a quiz, a course presentation or any of {{ skill.offered.length }} H5P types. Free, privacy first: no MCP server, no account." # The picture a shared link shows: the page's first screen at 1200×630, light, with the sample # settled. It is a screenshot, so take it again when that screen changes. socialImage: @@ -101,8 +102,8 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
  • -

    Built in your browser

    -

    The page builds and plays the activity on your computer. It is free and open source, needs no account, and sends nothing you make to this site.

    +

    Privacy first, no MCP server

    +

    Your chat builds the activity in its own sandbox, or this page builds and plays it on your computer: nothing you make is sent to us. It is free and open source, and needs no account.

  • @@ -140,7 +141,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
  • Play it, then download it

    -

    Claude and Grok build the .h5p file in the chat. ChatGPT hands over a link that opens the activity here, or its source, to open or paste below.

    +

    Claude and Grok build the .h5p file in the chat, and so does ChatGPT with the build archive attached. Without it, ChatGPT hands over a link that opens the activity here, or its source, to open or paste below.

  • diff --git a/src/h5p/create-with-ai/index.njk b/src/h5p/create-with-ai/index.njk index 7773f9c..a600680 100644 --- a/src/h5p/create-with-ai/index.njk +++ b/src/h5p/create-with-ai/index.njk @@ -65,9 +65,11 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o
  • ChatGPT: download h5p-author-chatgpt.zip and upload it - under Plugins. ChatGPT's code cannot reach the build tool, so it hands over a link to the - build page. If the page says the link arrived damaged, ask for the link again, or for the - source as a file, and open that on the build page. + under Plugins. ChatGPT's code cannot download the build tool, so download + h5p-author-build.tar.gz once too, + and attach it to the message where you ask: ChatGPT then builds the .h5p file in + the chat. Without it, ChatGPT hands over a link to the build page; if the page says the link + arrived damaged, ask for the source as a file and open that on the build page.
  • Grok: download @@ -89,6 +91,8 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o The zips hold h5p-author {{ skill.version }}, the release the build page runs: text files and nothing that runs, which you can read on GitHub. + The archive holds the same release's build tool, which runs in the chat's own sandbox, and + the H5P libraries it puts in the file, each under its own licence.

    @@ -115,10 +119,13 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o

    4 Get the .h5p file

    -

    What the chat hands you depends on what it can run:

    +

    + The build tool is a Node.js program, so what the chat hands you depends on whether its code + runs Node.js: +

      -
    • the .h5p file itself, from a chat whose code can reach the build tool, as Claude's and Grok's can;
    • -
    • a link that opens the activity on the build page, as ChatGPT gives;
    • +
    • the .h5p file itself, from a chat whose code runs Node.js and can reach the build tool, as Claude's and Grok's can, and ChatGPT's with the build archive attached;
    • +
    • a link that opens the activity on the build page, as ChatGPT gives without it;
    • or its source, as a file or a block of JSON: open or paste it on the build page and choose Build.

    @@ -238,8 +245,35 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o the pack's licence list says.

    - Nothing you make is sent to this site: the build page builds and plays it in your browser. - Your AI chat sees what you send it. + Privacy first: nothing you make is sent to this site, and there is no MCP server or other + server of ours in between. The chat builds the file in its own sandbox, or the build page + builds and plays it in your browser. Your AI chat sees what you send it, as it sees + anything you ask it. +

    +
    + +
    +

    With a local model, or offline

    +

    + A chat app for local models writes the activity as any chat does, but out of the box its code + does not run Node.js: +

    +
      +
    • Open WebUI's code tool runs Python;
    • +
    • LM Studio's code plugin runs JavaScript in Deno, not Node.js;
    • +
    • Jan has no code tool of its own.
    • +
    +

    + So there the chat hands over a link or the source, and the build page builds the file. Open + WebUI with Open Terminal is the exception: its default image has Node.js, which the build + needs. +

    +

    + With Node.js 22 or later on your own computer, the whole build can stay offline. Install the + skill in an agent with a terminal that runs a local model, from Ollama or LM Studio, and download + h5p-author-build.tar.gz once. + Then tell the agent where the archive is. It builds the .h5p file on your + computer, with no internet and nothing sent anywhere.

    @@ -253,6 +287,14 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o it, and ask in a new chat.

    +

    The chat says it cannot get the build tool

    +

    + Download h5p-author-build.tar.gz + and attach it to your next message: a chat whose code runs Node.js builds the file from it, + without installing or downloading anything. A chat whose code runs no Node.js gives you a + link to the build page instead. +

    +

    The build lists problems

    Choose Copy the problems for the chat, and paste them into the chat that wrote the source. It diff --git a/vite.config.ts b/vite.config.ts index 70d2fe7..aaf3e22 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,9 @@ +import { execFileSync } from 'node:child_process' import { readdirSync, readFileSync } from 'node:fs' import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { basename, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' +import { gzipSync } from 'node:zlib' import { buildPackage, writeBundles } from '@missing-elements/h5p-author' import { Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } from '@zip.js/zip.js' import { build, defineConfig, type Plugin } from 'vite' @@ -134,19 +136,27 @@ function sample(): Plugin { } /** - * `/h5p/bundles/`, for a chat that runs Node and can fetch a file but reaches no npm registry. The - * h5p-author skill builds there from two files fetched from here: the command line, and the - * bundle of the source's content type. Grok built that way on 2026-11-13, its `npx` having timed - * out. In ChatGPT's sandbox, which the bundles were first made for, the skill has not been seen - * to fetch either. + * `/h5p/bundles/`, for a chat that runs Node but reaches no npm registry. The h5p-author skill + * builds there from what it gets from here: * + * - `h5p-author-build.tar.gz`: the command line, every part of the pack with its index, and + * `libraries.txt`, which the person downloads once and attaches to a chat whose code can fetch + * nothing. ChatGPT's cannot: its download tool saved none of eleven files tried on 2026-11-13, + * ours or any site's, and an installed plugin's files never reach it. With this attached it + * built, as Claude and Grok did. Every type in one file, since it is downloaded before a type is + * chosen; a `.tar.gz`, since ChatGPT took the same files as a `.zip` for a plugin to install and + * refused it. * - `.zip`, one per content type in the pack (`h5p-author bundle --all`): `parts/`, the - * pack's whole index with the parts a build of that type can read, and `libraries.txt`. + * pack's whole index with the parts a build of that type can read, and `libraries.txt`. With + * the command line, two files a chat that can fetch takes from here: Grok built that way on + * 2026-11-13, its `npx` having timed out. * - `parts/`: every part of the pack under its own name, and its index. A bundle holds no addon, * so a build whose text calls for one stops and names its part, which the chat fetches from here. * - `h5p-author.mjs`: the command line as one file, everything it imports bundled in by Vite, so it * runs with nothing installed. It takes `--libraries`: the npm package's pinned pack is not with - * it, nor the package.json `--version` reads, so `index.json` says the versions. + * it. Since 0.9.8 its `--version` prints the versions from copies of the two package.json files + * Vite writes in (h5p-offline-player#198); before, it crashed, finding neither beside it. The + * build runs it and stops where it does not print the versions `index.json` says. * - `index.json`: the author's and the pack's versions, and each bundle's file, size and parts. * - `libraries.txt`: the pack's licence list, which asks to travel with its libraries. * @@ -189,12 +199,25 @@ function bundles(): Plugin { }, ssr: { noExternal: true, target: 'node' } }) + // The tool says its own versions, as a model checking it first asks: those this site pins. + const expected = `@missing-elements/h5p-author ${await version(author)}, library pack ${await version(libraries)}` + const printed = execFileSync(process.execPath, [join(folder, 'h5p-author.mjs'), '--version'], { encoding: 'utf8' }).trim() + if (printed !== expected) throw new Error(`h5p-author.mjs --version printed "${printed}", not "${expected}"`) + // The archive lays them out as a build from the bundles does: `tar -xzf`, then `--libraries parts`. + const archive = tarGz([ + ['h5p-author.mjs', await readFile(join(folder, 'h5p-author.mjs'))], + ['libraries.txt', await readFile(join(folder, 'libraries.txt'))], + ['parts/index.json', await readFile(join(parts, 'index.json'))], + ...(await Promise.all(index.libraries.map(async ({ folder: part }) => [`parts/${part}.h5p`, await readFile(join(parts, `${part}.h5p`))] as const))) + ]) + await writeFile(join(folder, ARCHIVE), archive) const listing = { author: await version(author), pack: await version(libraries), tool: 'h5p-author.mjs', parts: 'parts/', licences: 'libraries.txt', + archive: { file: ARCHIVE, size: archive.length }, bundles: written.map(({ output, type, parts, size }) => { const [machineName, typeVersion] = type.split(' ') return { type: machineName, version: typeVersion, file: basename(output), size, parts: parts.length } @@ -208,6 +231,52 @@ function bundles(): Plugin { /** One date for every entry, as `h5p-author bundle` writes its zips: the same pin writes the same bytes. */ const ZIP_DATE = new Date(2000, 0, 1, 2) +/** The archive in `/h5p/bundles/` that a person attaches to a chat (see `bundles()`). */ +const ARCHIVE = 'h5p-author-build.tar.gz' + +/** + * A gzipped ustar archive of the files, with an entry for each folder before what is in it, every + * date `ZIP_DATE`'s day and no owner: the same files write the same bytes on any computer. Node has + * gzip and no tar; ustar is a 512-byte header per entry, and every tar since 1988 reads it. + */ +function tarGz(files: ReadonlyArray): Buffer { + const date = Date.UTC(2000, 0, 1) / 1000 + const header = (name: string, size: number, type: '0' | '5') => { + if (Buffer.byteLength(name) > 100) throw new Error(`${name}: a ustar name holds 100 bytes`) + const block = Buffer.alloc(512) + block.write(name, 0, 'utf8') + block.write(type === '5' ? '0000755\0' : '0000644\0', 100, 'ascii') + block.write('0000000\0', 108, 'ascii') + block.write('0000000\0', 116, 'ascii') + block.write(`${size.toString(8).padStart(11, '0')}\0`, 124, 'ascii') + block.write(`${date.toString(8).padStart(11, '0')}\0`, 136, 'ascii') + block.write(' '.repeat(8), 148, 'ascii') + block.write(type, 156, 'ascii') + block.write('ustar\0', 257, 'ascii') + block.write('00', 263, 'ascii') + let sum = 0 + for (const byte of block) sum += byte + block.write(`${sum.toString(8).padStart(6, '0')}\0 `, 148, 'ascii') + return block + } + const chunks: Buffer[] = [] + const folders = new Set() + for (const [path, bytes] of files) { + const parts = path.split('/') + for (let depth = 1; depth < parts.length; depth++) { + const name = `${parts.slice(0, depth).join('/')}/` + if (folders.has(name)) continue + folders.add(name) + chunks.push(header(name, 0, '5')) + } + chunks.push(header(path, bytes.length, '0'), Buffer.from(bytes), Buffer.alloc((512 - (bytes.length % 512)) % 512)) + } + chunks.push(Buffer.alloc(1024)) + const gz = gzipSync(Buffer.concat(chunks), { level: 9 }) + gz[9] = 3 // the header's OS byte, Unix, which zlib otherwise takes from the platform it was built on + return gz +} + /** * The h5p-author skill as zips to install, beside the files `/h5p/skill/` serves, from the same * pinned author package: @@ -223,7 +292,9 @@ const ZIP_DATE = new Date(2000, 0, 1, 2) * * Both were uploaded as they are written here, by the owner on 2026-11-13, the first into Claude * and into Grok. ChatGPT's code cannot read a plugin's files, so neither carries the build tool: - * where a chat's code cannot reach it, the build page builds. + * where a chat's code cannot reach it, the person attaches `bundles()`'s archive, or the build page + * builds. The plugin's description leads with what sets it apart, in the owner's words: privacy + * first, no MCP server, so nothing a person makes passes through a server of ours. */ function skillZips(): Plugin { const author = new URL('./node_modules/@missing-elements/h5p-author/', import.meta.url) @@ -247,7 +318,7 @@ function skillZips(): Plugin { $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', name: 'h5p-author', version, - description: 'Create H5P content from a prompt, a lesson or supplied material, as a playable .h5p: quizzes, flash cards, presentations, interactive books and more.', + description: 'Privacy first, no MCP server: create H5P content from a prompt, a lesson or supplied material, as a playable .h5p (quizzes, flash cards, presentations, interactive books and more). It is built in your chat\'s own sandbox or in your browser, and nothing you make is sent to us.', author: { name: 'Missing Elements', url: 'https://github.com/missing-elements' }, homepage: guide, repository: 'https://github.com/missing-elements/h5p-offline-player', @@ -257,7 +328,7 @@ function skillZips(): Plugin { 'com.openai': { interface: { displayName: 'H5P Author by Missing Elements', - shortDescription: 'Make a playable H5P activity from a prompt or your material', + shortDescription: 'H5P activities from a prompt: privacy first, no MCP server', developerName: 'Missing Elements', websiteURL: guide }