Skip to content

Commit 8cf0599

Browse files
committed
The build archive for chats whose code can download nothing, and privacy first
bundles() writes /h5p/bundles/h5p-author-build.tar.gz: the one-file tool, every part of the pack and libraries.txt, the same bytes on every build. The guide says to attach it in ChatGPT, that the tool is a Node.js program, what local-model chat apps run, and how to build offline; the build page and the ChatGPT plugin lead with privacy first, no MCP server. Closes #47.
1 parent f1071b4 commit 8cf0599

4 files changed

Lines changed: 132 additions & 25 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ The site of the [missing-elements](https://github.com/missing-elements) web comp
1111
| `/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/) |
1212
| `/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/) |
1313
| `/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) |
14-
| `/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) |
14+
| `/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) |
1515
| `/h5p/build/types/` | The skill's type files, which the build page copies for the chat | written by [vite.config.ts](vite.config.ts) |
1616

17-
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.
17+
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.
1818

1919
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.
2020

‎src/h5p/build/index.njk‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
---
22
layout: base.njk
33
# The title and description carry what people search for: an AI H5P generator, with ChatGPT and the
4-
# like. The description counts the types as the hero does, in the release the page runs.
5-
title: "Free AI H5P generator: make H5P with ChatGPT, Claude or Grok"
4+
# like, and what sets it apart, in the owner's words: privacy first, no MCP server. The description
5+
# counts the types as the hero does, in the release the page runs.
6+
title: "Free AI H5P generator, privacy first: make H5P with ChatGPT, Claude or Grok"
67
eleventyComputed:
7-
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."
8+
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."
89
# The picture a shared link shows: the page's first screen at 1200×630, light, with the sample
910
# settled. It is a screenshot, so take it again when that screen changes.
1011
socialImage:
@@ -101,8 +102,8 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
101102
</li>
102103
<li class="feature">
103104
<span class="feature-icon icon-browser" aria-hidden="true"></span>
104-
<h3>Built in your browser</h3>
105-
<p>The page builds and plays the activity on your computer. It is free and <a href="https://github.com/missing-elements/missing-elements.github.io/tree/main/src/h5p/build">open source</a>, needs no account, and sends nothing you make to this site.</p>
105+
<h3>Privacy first, no MCP server</h3>
106+
<p>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 <a href="https://github.com/missing-elements/missing-elements.github.io/tree/main/src/h5p/build">open source</a>, and needs no account.</p>
106107
</li>
107108
<li class="feature">
108109
<span class="feature-icon icon-school" aria-hidden="true"></span>
@@ -140,7 +141,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' da
140141
</li>
141142
<li>
142143
<h3>Play it, then download it</h3>
143-
<p>Claude and Grok build the <code>.h5p</code> file in the chat. ChatGPT hands over a link that opens the activity here, or its source, to <a href="#build-it">open or paste below</a>.</p>
144+
<p>Claude and Grok build the <code>.h5p</code> file in the chat, and so does ChatGPT with <a href="/h5p/create-with-ai/#install">the build archive</a> attached. Without it, ChatGPT hands over a link that opens the activity here, or its source, to <a href="#build-it">open or paste below</a>.</p>
144145
</li>
145146
</ol>
146147
<div class="prompt-box">

‎src/h5p/create-with-ai/index.njk‎

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,11 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o
6565
<li>
6666
<strong>ChatGPT</strong>: download
6767
<a href="/h5p/skill/h5p-author-chatgpt.zip" download>h5p-author-chatgpt.zip</a> and upload it
68-
under Plugins. ChatGPT's code cannot reach the build tool, so it hands over a link to the
69-
build page. If the page says the link arrived damaged, ask for the link again, or for the
70-
source as a file, and open that on the build page.
68+
under Plugins. ChatGPT's code cannot download the build tool, so download
69+
<a href="/h5p/bundles/h5p-author-build.tar.gz" download>h5p-author-build.tar.gz</a> once too,
70+
and attach it to the message where you ask: ChatGPT then builds the <code>.h5p</code> file in
71+
the chat. Without it, ChatGPT hands over a link to the build page; if the page says the link
72+
arrived damaged, ask for the source as a file and open that on the build page.
7173
</li>
7274
<li>
7375
<strong>Grok</strong>: download
@@ -89,6 +91,8 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o
8991
The zips hold h5p-author {{ skill.version }}, the release the build page runs: text files and
9092
nothing that runs, which you can read
9193
<a href="https://github.com/missing-elements/h5p-offline-player/tree/main/skills/h5p-author">on GitHub</a>.
94+
The archive holds the same release's build tool, which runs in the chat's own sandbox, and
95+
the H5P libraries it puts in the file, each under its own licence.
9296
</p>
9397
</section>
9498

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

116120
<section class="step" aria-labelledby="file-heading">
117121
<h2 id="file-heading"><span class="step-marker"><span class="step-number">4</span></span> Get the .h5p file</h2>
118-
<p>What the chat hands you depends on what it can run:</p>
122+
<p>
123+
The build tool is a Node.js program, so what the chat hands you depends on whether its code
124+
runs Node.js:
125+
</p>
119126
<ul>
120-
<li>the <code>.h5p</code> file itself, from a chat whose code can reach the build tool, as Claude's and Grok's can;</li>
121-
<li>a link that opens the activity on the build page, as ChatGPT gives;</li>
127+
<li>the <code>.h5p</code> 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;</li>
128+
<li>a link that opens the activity on the build page, as ChatGPT gives without it;</li>
122129
<li>or its source, as a file or a block of JSON: open or paste it <a href="/h5p/build/#build-it">on the build page</a> and choose Build.</li>
123130
</ul>
124131
<p>
@@ -238,8 +245,35 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o
238245
<a href="/h5p/assets/libraries-LICENSES.txt">the pack's licence list</a> says.
239246
</p>
240247
<p>
241-
Nothing you make is sent to this site: the build page builds and plays it in your browser.
242-
Your AI chat sees what you send it.
248+
Privacy first: nothing you make is sent to this site, and there is no MCP server or other
249+
server of ours in between. The chat builds the file in its own sandbox, or the build page
250+
builds and plays it in your browser. Your AI chat sees what you send it, as it sees
251+
anything you ask it.
252+
</p>
253+
</section>
254+
255+
<section aria-labelledby="local">
256+
<h2 id="local">With a local model, or offline</h2>
257+
<p>
258+
A chat app for local models writes the activity as any chat does, but out of the box its code
259+
does not run Node.js:
260+
</p>
261+
<ul>
262+
<li>Open WebUI's code tool runs Python;</li>
263+
<li>LM Studio's code plugin runs JavaScript in Deno, not Node.js;</li>
264+
<li>Jan has no code tool of its own.</li>
265+
</ul>
266+
<p>
267+
So there the chat hands over a link or the source, and the build page builds the file. Open
268+
WebUI with Open Terminal is the exception: its default image has Node.js, which the build
269+
needs.
270+
</p>
271+
<p>
272+
With Node.js on your own computer, the whole build can stay offline. Install the skill in an
273+
agent with a terminal that runs a local model, from Ollama or LM Studio, and download
274+
<a href="/h5p/bundles/h5p-author-build.tar.gz" download>h5p-author-build.tar.gz</a> once.
275+
Then tell the agent where the archive is. It builds the <code>.h5p</code> file on your
276+
computer, with no internet and nothing sent anywhere.
243277
</p>
244278
</section>
245279

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

290+
<h3>The chat says it cannot get the build tool</h3>
291+
<p>
292+
Download <a href="/h5p/bundles/h5p-author-build.tar.gz" download>h5p-author-build.tar.gz</a>
293+
and attach it to your next message: a chat whose code runs Node.js builds the file from it,
294+
without installing or downloading anything. A chat whose code runs no Node.js gives you a
295+
link to the build page instead.
296+
</p>
297+
256298
<h3>The build lists problems</h3>
257299
<p>
258300
Choose Copy the problems for the chat, and paste them into the chat that wrote the source. It

‎vite.config.ts‎

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { readdirSync, readFileSync } from 'node:fs'
22
import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
33
import { basename, join, resolve } from 'node:path'
44
import { fileURLToPath } from 'node:url'
5+
import { gzipSync } from 'node:zlib'
56
import { buildPackage, writeBundles } from '@missing-elements/h5p-author'
67
import { Uint8ArrayReader, Uint8ArrayWriter, ZipWriter } from '@zip.js/zip.js'
78
import { build, defineConfig, type Plugin } from 'vite'
@@ -134,14 +135,20 @@ function sample(): Plugin {
134135
}
135136

136137
/**
137-
* `/h5p/bundles/`, for a chat that runs Node and can fetch a file but reaches no npm registry. The
138-
* h5p-author skill builds there from two files fetched from here: the command line, and the
139-
* bundle of the source's content type. Grok built that way on 2026-11-13, its `npx` having timed
140-
* out. In ChatGPT's sandbox, which the bundles were first made for, the skill has not been seen
141-
* to fetch either.
138+
* `/h5p/bundles/`, for a chat that runs Node but reaches no npm registry. The h5p-author skill
139+
* builds there from what it gets from here:
142140
*
141+
* - `h5p-author-build.tar.gz`: the command line, every part of the pack with its index, and
142+
* `libraries.txt`, which the person downloads once and attaches to a chat whose code can fetch
143+
* nothing. ChatGPT's cannot: its download tool saved none of eleven files tried on 2026-11-13,
144+
* ours or any site's, and an installed plugin's files never reach it. With this attached it
145+
* built, as Claude and Grok did. Every type in one file, since it is downloaded before a type is
146+
* chosen; a `.tar.gz`, since ChatGPT took the same files as a `.zip` for a plugin to install and
147+
* refused it.
143148
* - `<type>.zip`, one per content type in the pack (`h5p-author bundle --all`): `parts/`, the
144-
* pack's whole index with the parts a build of that type can read, and `libraries.txt`.
149+
* pack's whole index with the parts a build of that type can read, and `libraries.txt`. With
150+
* the command line, two files a chat that can fetch takes from here: Grok built that way on
151+
* 2026-11-13, its `npx` having timed out.
145152
* - `parts/`: every part of the pack under its own name, and its index. A bundle holds no addon,
146153
* so a build whose text calls for one stops and names its part, which the chat fetches from here.
147154
* - `h5p-author.mjs`: the command line as one file, everything it imports bundled in by Vite, so it
@@ -189,12 +196,21 @@ function bundles(): Plugin {
189196
},
190197
ssr: { noExternal: true, target: 'node' }
191198
})
199+
// The archive lays them out as a build from the bundles does: `tar -xzf`, then `--libraries parts`.
200+
const archive = tarGz([
201+
['h5p-author.mjs', await readFile(join(folder, 'h5p-author.mjs'))],
202+
['libraries.txt', await readFile(join(folder, 'libraries.txt'))],
203+
['parts/index.json', await readFile(join(parts, 'index.json'))],
204+
...(await Promise.all(index.libraries.map(async ({ folder: part }) => [`parts/${part}.h5p`, await readFile(join(parts, `${part}.h5p`))] as const)))
205+
])
206+
await writeFile(join(folder, ARCHIVE), archive)
192207
const listing = {
193208
author: await version(author),
194209
pack: await version(libraries),
195210
tool: 'h5p-author.mjs',
196211
parts: 'parts/',
197212
licences: 'libraries.txt',
213+
archive: { file: ARCHIVE, size: archive.length },
198214
bundles: written.map(({ output, type, parts, size }) => {
199215
const [machineName, typeVersion] = type.split(' ')
200216
return { type: machineName, version: typeVersion, file: basename(output), size, parts: parts.length }
@@ -208,6 +224,52 @@ function bundles(): Plugin {
208224
/** One date for every entry, as `h5p-author bundle` writes its zips: the same pin writes the same bytes. */
209225
const ZIP_DATE = new Date(2000, 0, 1, 2)
210226

227+
/** The archive in `/h5p/bundles/` that a person attaches to a chat (see `bundles()`). */
228+
const ARCHIVE = 'h5p-author-build.tar.gz'
229+
230+
/**
231+
* A gzipped ustar archive of the files, with an entry for each folder before what is in it, every
232+
* date `ZIP_DATE`'s day and no owner: the same files write the same bytes on any computer. Node has
233+
* gzip and no tar; ustar is a 512-byte header per entry, and every tar since 1988 reads it.
234+
*/
235+
function tarGz(files: ReadonlyArray<readonly [string, Uint8Array]>): Buffer {
236+
const date = Date.UTC(2000, 0, 1) / 1000
237+
const header = (name: string, size: number, type: '0' | '5') => {
238+
if (Buffer.byteLength(name) > 100) throw new Error(`${name}: a ustar name holds 100 bytes`)
239+
const block = Buffer.alloc(512)
240+
block.write(name, 0, 'utf8')
241+
block.write(type === '5' ? '0000755\0' : '0000644\0', 100, 'ascii')
242+
block.write('0000000\0', 108, 'ascii')
243+
block.write('0000000\0', 116, 'ascii')
244+
block.write(`${size.toString(8).padStart(11, '0')}\0`, 124, 'ascii')
245+
block.write(`${date.toString(8).padStart(11, '0')}\0`, 136, 'ascii')
246+
block.write(' '.repeat(8), 148, 'ascii')
247+
block.write(type, 156, 'ascii')
248+
block.write('ustar\0', 257, 'ascii')
249+
block.write('00', 263, 'ascii')
250+
let sum = 0
251+
for (const byte of block) sum += byte
252+
block.write(`${sum.toString(8).padStart(6, '0')}\0 `, 148, 'ascii')
253+
return block
254+
}
255+
const chunks: Buffer[] = []
256+
const folders = new Set<string>()
257+
for (const [path, bytes] of files) {
258+
const parts = path.split('/')
259+
for (let depth = 1; depth < parts.length; depth++) {
260+
const name = `${parts.slice(0, depth).join('/')}/`
261+
if (folders.has(name)) continue
262+
folders.add(name)
263+
chunks.push(header(name, 0, '5'))
264+
}
265+
chunks.push(header(path, bytes.length, '0'), Buffer.from(bytes), Buffer.alloc((512 - (bytes.length % 512)) % 512))
266+
}
267+
chunks.push(Buffer.alloc(1024))
268+
const gz = gzipSync(Buffer.concat(chunks), { level: 9 })
269+
gz[9] = 3 // the header's OS byte, Unix, which zlib otherwise takes from the platform it was built on
270+
return gz
271+
}
272+
211273
/**
212274
* The h5p-author skill as zips to install, beside the files `/h5p/skill/` serves, from the same
213275
* pinned author package:
@@ -223,7 +285,9 @@ const ZIP_DATE = new Date(2000, 0, 1, 2)
223285
*
224286
* Both were uploaded as they are written here, by the owner on 2026-11-13, the first into Claude
225287
* and into Grok. ChatGPT's code cannot read a plugin's files, so neither carries the build tool:
226-
* where a chat's code cannot reach it, the build page builds.
288+
* where a chat's code cannot reach it, the person attaches `bundles()`'s archive, or the build page
289+
* builds. The plugin's description leads with what sets it apart, in the owner's words: privacy
290+
* first, no MCP server, so nothing a person makes passes through a server of ours.
227291
*/
228292
function skillZips(): Plugin {
229293
const author = new URL('./node_modules/@missing-elements/h5p-author/', import.meta.url)
@@ -247,7 +311,7 @@ function skillZips(): Plugin {
247311
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
248312
name: 'h5p-author',
249313
version,
250-
description: 'Create H5P content from a prompt, a lesson or supplied material, as a playable .h5p: quizzes, flash cards, presentations, interactive books and more.',
314+
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.',
251315
author: { name: 'Missing Elements', url: 'https://github.com/missing-elements' },
252316
homepage: guide,
253317
repository: 'https://github.com/missing-elements/h5p-offline-player',
@@ -257,7 +321,7 @@ function skillZips(): Plugin {
257321
'com.openai': {
258322
interface: {
259323
displayName: 'H5P Author by Missing Elements',
260-
shortDescription: 'Make a playable H5P activity from a prompt or your material',
324+
shortDescription: 'H5P activities from a prompt: privacy first, no MCP server',
261325
developerName: 'Missing Elements',
262326
websiteURL: guide
263327
}

0 commit comments

Comments
 (0)