From 7e66f34158c4bffc86a67e957031d0d49ae17cf6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 17:48:04 +0000 Subject: [PATCH 01/11] The open-source setup is two commands: init --agent, and the docs, the MCP server's setup text and /start/open-source say so init --agent adds the dev-only agent link next to the import it writes (every framework; one if block in the files it creates for Next.js; plain HTML gets a module script after the tag that loads dist/agent.js from the script URL's directory on localhost), and remove takes it out, wrapped by a formatter or not. The READMEs' quick start, INSTALL.md ("How do I set up the open-source parts only?"), the FAQ, AGENT.md, the MCP server's README and setup text, and the start page's prompt and commands name the two commands (npx @genclass/runtime@beta init --no-telemetry --agent, then npx -y @genclass/mcp@0.2.1 init) instead of a nine-step hand edit; every @genclass/mcp launcher stays pinned, with INSTALL.md and FAQ.md added to the sync list. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Fqwfq3CizvxvgyXp6wY8WR --- FAQ.md | 7 ++ README.md | 12 ++++ docs/runtime/AGENT.md | 4 +- docs/runtime/INSTALL.md | 25 +++++++ packages/mcp/README.md | 7 +- packages/mcp/src/tools.ts | 1 + packages/mcp/test/manifests.test.ts | 2 + packages/runtime/CHANGELOG.md | 14 ++++ packages/runtime/README.md | 23 +++++- packages/runtime/bin/genclass-runtime.mjs | 4 +- packages/runtime/bin/lib/edit.mjs | 21 ++++-- packages/runtime/bin/lib/init.mjs | 13 +++- packages/runtime/bin/lib/plan.mjs | 63 +++++++++++----- packages/runtime/test/install/cli.test.ts | 85 ++++++++++++++++++++++ telemetry-worker/src/pages.ts | 82 +++++++-------------- telemetry-worker/test/bundle.test.ts | 2 +- telemetry-worker/test/pages.test.ts | 87 +++++++++++------------ 17 files changed, 314 insertions(+), 138 deletions(-) diff --git a/FAQ.md b/FAQ.md index d27cda35..3696f3dc 100644 --- a/FAQ.md +++ b/FAQ.md @@ -171,6 +171,13 @@ Nothing is sold on the site. The pricing page shows the plans in force. Details: [genclass.dev/pricing](https://genclass.dev/pricing), [billing/plans.json](https://github.com/genclass-dev/GenClass-lib/blob/main/billing/plans.json). +## Can I use it without an account? + +Yes. Two commands set up every open-source part with nothing sent to genclass.dev: +`npx @genclass/runtime@beta init --no-telemetry --agent` (the runtime in observe mode, the dev-only overlay and agent +link, no token) and `npx -y @genclass/mcp@0.2.1 init` (the MCP server and the `genclass` skill for Claude Code). Details: +[INSTALL.md](https://github.com/genclass-dev/GenClass-lib/blob/main/docs/runtime/INSTALL.md#how-do-i-set-up-the-open-source-parts-only). + ## How is it different from Sentry, Datadog or my tests? Error trackers and APM collect exceptions, traces and timings; a stale response or a double submit usually throws diff --git a/README.md b/README.md index 9dd8c468..16b122a3 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,18 @@ Once you approve, `init` installs the package, adds one import first in your ent devtools overlay in development only) and creates your app's dashboard token (`--no-token` skips it; every flag: [INSTALL.md](docs/runtime/INSTALL.md)). +**Open source only, no account.** Two commands set up every open-source part, with nothing sent to genclass.dev: + +```bash +npx @genclass/runtime@beta init --no-telemetry --agent # the runtime (observe), the dev-only overlay and agent link; no token +npx -y @genclass/mcp@0.2.1 init # the GenClass MCP server and the `genclass` skill for Claude Code +``` + +The first adds one marked import (or script tag) plus an option file with `telemetry: false`; the second writes +`.mcp.json` and `.claude/skills/genclass/SKILL.md` (other agents: `npx -y @genclass/mcp@0.2.1 config `). Nothing +else is needed; `npx @genclass/runtime@beta remove` undoes the first +([the open-source setup](docs/runtime/INSTALL.md#how-do-i-set-up-the-open-source-parts-only)). + Or by hand: `npm install @genclass/runtime@beta`, then first in your entry file (any bundler; Next.js 15.3 or later: `instrumentation-client.ts`, earlier versions: a client component in the root layout or `pages/_app`): diff --git a/docs/runtime/AGENT.md b/docs/runtime/AGENT.md index 52e23da7..ffef4fbc 100644 --- a/docs/runtime/AGENT.md +++ b/docs/runtime/AGENT.md @@ -57,7 +57,9 @@ Every launcher is pinned to the release. `packages/mcp/server.json` is the entry (published by the release workflow with GitHub OIDC after the npm package); the listings for other directories are drafted in [docs/adoption/listings/mcp.md](../adoption/listings/mcp.md). -In the app's browser entry, development builds only (`@genclass/runtime` 0.3.0-beta.1 or later): +`npx @genclass/runtime@beta init --agent` (0.3.0-beta.6) adds the line for you, next to the import it writes (plain HTML: a +module script after the tag). By hand, in the app's browser entry, development builds only (`@genclass/runtime` +0.3.0-beta.1 or later): ```ts import { GenClass } from "@genclass/runtime"; diff --git a/docs/runtime/INSTALL.md b/docs/runtime/INSTALL.md index c71c1fa0..5febcb77 100644 --- a/docs/runtime/INSTALL.md +++ b/docs/runtime/INSTALL.md @@ -60,6 +60,7 @@ Content-Security-Policy (`next.config` headers, a proxy or middleware, a `` | both | the project directory (default: the current directory) | | `--from ` | init | install `@genclass/runtime` from this spec (a version, tag or tarball path) | | `--cdn ` | init | plain HTML: the script URL (default: jsDelivr, this version, with an SRI hash; this version's unpkg URL gets one too; other URLs get no hash) | @@ -136,6 +137,30 @@ reached, `init` says so, sets GenClass up without a token, and points you to [genclass.dev/start](https://genclass.dev/start); run `init --token gc_...` later. `--dry-run` sends nothing and installs nothing. +## How do I set up the open-source parts only? + +Two commands, no account, no token, nothing sent to genclass.dev: + +```bash +npx @genclass/runtime@beta init --no-telemetry --agent # the runtime in observe, the dev-only overlay and agent link +npx -y @genclass/mcp@0.2.1 init # the GenClass MCP server and the `genclass` skill for Claude Code +``` + +The first finds your framework, installs `@genclass/runtime`, adds one marked import (plain HTML: the script tag, +pinned with an integrity hash) with the dev-only devtools and agent lines, and writes `telemetry: false` into a +marked `genclass.config.ts` (or `.js`) next to your entry (`data-telemetry="off"` on a script tag), so GenClass's +anonymous diagnostics are off; `--no-telemetry` implies `--no-token`. Browsers still download the model and +onnxruntime-web, public files, from jsDelivr unless you serve them (`--model-url`, +[Content-Security-Policy and self-hosting](../../packages/runtime/README.md#content-security-policy-and-self-hosting)). +Add `--mode guard` to let GenClass act, `--sink console-json` for findings in your own logs. + +The second writes the server into `.mcp.json` and the `genclass` skill (the procedure Claude Code follows for timing +bugs) to `.claude/skills/genclass/SKILL.md`; it runs on your machine and needs no account +([`@genclass/mcp`](../../packages/mcp/README.md); other agents: `npx -y @genclass/mcp@0.2.1 config `). Run the dev +server: until the MCP server runs, the page logs one failed request to `127.0.0.1:47357`, the agent link finding no +server. `npx @genclass/runtime@beta remove` takes the first command's changes out; delete `.mcp.json`'s entry and +`.claude/skills/genclass/` for the second. + ## How do I install it by hand? **One import** (any bundler; the package is ESM only, there is no `require` build), first in your entry file, so it diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 8e2c4ad8..5823f316 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -307,9 +307,10 @@ read a report, what to say when done, what to do when the tools are missing) is To remove it: delete `.claude/skills/genclass/` (or the directory the skill tool wrote to); with the plugin, `/plugin uninstall genclass@genclass`. -Then connect the app's development build (once). The agent link needs the newest runtime line, `@genclass/runtime` -0.3.0-beta.1 or later: `npm install @genclass/runtime@beta` (a bare `npm install @genclass/runtime` gets npm's -`latest`, the older 0.2.x, which has no agent entry): +Then connect the app's development build (once): `npx @genclass/runtime@beta init --agent` adds a dev-only line next to +the import it writes (plain HTML: a module script after the tag), installing the runtime first if needed. By hand, the +agent link needs the newest runtime line, `@genclass/runtime` 0.3.0-beta.1 or later: `npm install @genclass/runtime@beta` +(a bare `npm install @genclass/runtime` gets npm's `latest`, the older 0.2.x, which has no agent entry): ```ts import { GenClass } from "@genclass/runtime"; diff --git a/packages/mcp/src/tools.ts b/packages/mcp/src/tools.ts index 47aa1314..380e8390 100644 --- a/packages/mcp/src/tools.ts +++ b/packages/mcp/src/tools.ts @@ -559,6 +559,7 @@ export function setupText(cwd: string, client?: string | null): string { const guard = next ? `process.env.NODE_ENV === "development"` : vite ? "import.meta.env.DEV" : `process.env.NODE_ENV !== "production"`; const where = next ? "a client component loaded once from the root layout (\"use client\")" : vite ? "the app's entry (src/main.tsx or src/main.ts), before the app renders" : "the app's browser entry, before the app starts"; const lines = [ + "One command does the app side: npx @genclass/runtime@beta init --agent (it installs the runtime if needed and adds a dev-only agent line next to the import it writes; --no-telemetry too for no diagnostics). By hand:", installed ? "@genclass/runtime is installed." : `1. Install the newest runtime, 0.3.0-beta.1 or later (the agent entry and analytics; 0.2.x has neither${has("@genclass/runtime") ? `; this project has ${JSON.stringify(range)}${have !== range ? `, installed ${have}` : ""}` : ""}): npm install @genclass/runtime@beta`, diff --git a/packages/mcp/test/manifests.test.ts b/packages/mcp/test/manifests.test.ts index c8204f2c..787b3ab9 100644 --- a/packages/mcp/test/manifests.test.ts +++ b/packages/mcp/test/manifests.test.ts @@ -76,6 +76,8 @@ const PINS: Pin[] = [ { file: "docs/runtime/AGENT.md", text: (s) => s.replace(PINNED, pinned(V)) }, { file: "README.md", text: (s) => s.replace(PINNED, pinned(V)) }, { file: "packages/runtime/README.md", text: (s) => s.replace(PINNED, pinned(V)) }, + { file: "docs/runtime/INSTALL.md", text: (s) => s.replace(PINNED, pinned(V)) }, + { file: "FAQ.md", text: (s) => s.replace(PINNED, pinned(V)) }, ]; function read(file: string): string { diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 3cdd1fd8..42d2bf81 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## Unreleased (0.3.0-beta.6) + +- **The open-source setup is two commands** (`bin/lib/`, owner request 2026-11-02). `init --agent` adds the dev-only + agent link next to the import it writes, the same way it adds the devtools line: `if () + import("@genclass/runtime/agent").then((a) => a.connectAgent(genclass))` in every framework it knows (one `if` block + in the files it creates for Next.js; plain HTML gets a module script after the tag, which loads `dist/agent.js` from + the script URL's directory on `localhost` and connects `GenClass.runtime`; module scripts run after deferred ones, + in order). `remove` takes it out, wrapped by a formatter or not, and refuses an edited one. So + `npx @genclass/runtime@beta init --no-telemetry --agent` and `npx -y @genclass/mcp init` set up every open-source + part with no account and nothing sent, and the READMEs, INSTALL.md ("How do I set up the open-source parts only?"), + FAQ, AGENT.md, the MCP server's setup text and genclass.dev/start/open-source say so with those two commands instead + of a nine-step hand edit. No runtime code, no model-visible text, no change to what is sent. Tests: + `test/install/cli.test.ts` ("init --agent"). + ## 0.3.0-beta.5 (2026-11-02) Security fixes from the adversarial review of the mode-raise fix (`0.3.0-beta.2`); the same four fixes are in `0.2.3` diff --git a/packages/runtime/README.md b/packages/runtime/README.md index 2c8b8f4f..965a30e7 100644 --- a/packages/runtime/README.md +++ b/packages/runtime/README.md @@ -35,6 +35,18 @@ Once you approve the diff, `init` also creates your app's free dashboard at genc `package.json` `name`; `--no-token`, `--token`, `--no-telemetry` and `--dry-run` create no dashboard and never contact genclass.dev ([Your dashboard](#your-dashboard)). +**Open source only, no account.** Two commands set up every open-source part, with nothing sent to genclass.dev: + +```bash +npx @genclass/runtime@beta init --no-telemetry --agent # the runtime (observe), the dev-only overlay and agent link; no token +npx -y @genclass/mcp@0.2.1 init # the GenClass MCP server and the `genclass` skill for Claude Code +``` + +The first adds one marked import (or script tag) plus an option file with `telemetry: false`; the second writes +`.mcp.json` and `.claude/skills/genclass/SKILL.md` (other agents: `npx -y @genclass/mcp@0.2.1 config `). Nothing +else is needed; `npx @genclass/runtime@beta remove` undoes the first +([the open-source setup](https://github.com/genclass-dev/GenClass-lib/blob/main/docs/runtime/INSTALL.md#how-do-i-set-up-the-open-source-parts-only)). + Or by hand: ```bash @@ -468,7 +480,9 @@ npx @genclass/runtime@beta remove # undo exactly what init added Other flags: `--yes`, `--no-telemetry` (`--telemetry` undoes it on a later run and writes no telemetry option, so the default applies: on, but off on local hosts), `--no-token`, `--token gc_...`, `--sink`, `--model-url`, `--no-install`, -`--no-devtools`, and from `0.3.0-beta.2` `--no-network` and `--sync` (plain HTML). +`--no-devtools`, `--agent` (from `0.3.0-beta.6`: the dev-only agent link too, so your coding agent sees the dev +pages; [Your coding agent](#your-coding-agent-replay-and-explore)), and from `0.3.0-beta.2` `--no-network` and +`--sync` (plain HTML). **Which version** (from `0.3.0-beta.2`). `init` (and `login`, `connect`) first prints its own version and the npm dist-tag it came from, asks npm's dist-tags once (`npm view @genclass/runtime dist-tags`, at most 3 s; never with @@ -675,9 +689,14 @@ Claude Code and other coding agents, and lets the agent: - **keep the proof:** a generated Playwright test that sets the same order and fails while the bug is there. ```bash -claude mcp add genclass -- npx -y @genclass/mcp@0.2.1 +npx @genclass/runtime@beta init --agent # the app side: a dev-only line next to the import init writes (0.3.0-beta.6) +npx -y @genclass/mcp@0.2.1 init # the server in .mcp.json and the `genclass` skill, for Claude Code ``` +Other agents: `npx -y @genclass/mcp@0.2.1 config `, or `claude mcp add genclass -- npx -y @genclass/mcp@0.2.1`. By +hand, the app side is one line in a development build (plain HTML: a module script after the tag, which `init --agent` +writes too): + ```ts import { GenClass } from "@genclass/runtime"; import { connectAgent } from "@genclass/runtime/agent"; diff --git a/packages/runtime/bin/genclass-runtime.mjs b/packages/runtime/bin/genclass-runtime.mjs index e635c6c5..b374f389 100755 --- a/packages/runtime/bin/genclass-runtime.mjs +++ b/packages/runtime/bin/genclass-runtime.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node // genclass-runtime CLI (Node >= 20, no dependencies). Also runs as `npx @genclass/runtime `. // -// genclass-runtime init [--mode observe|guard|heal] [--token | --no-token] [--yes] [--dry-run] [--no-install] [--no-devtools] +// genclass-runtime init [--mode observe|guard|heal] [--token | --no-token] [--agent] [--yes] [--dry-run] [--no-install] [--no-devtools] // genclass-runtime remove [--yes] [--dry-run] [--keep-package] // Set GenClass up in a project (framework detection, install, one marked import in the entry file, an app token // and private dashboard link from genclass.dev) and take it out again. Implemented in bin/lib/ (owner: INSTALL). @@ -57,7 +57,7 @@ const ORT_FILES = { const ROLES = ["tokenizer", "calibration", "meta"]; const USAGE = `Usage: - genclass-runtime init [--mode observe|guard|heal] [--token | --no-token] [--yes] [--dry-run] [--no-install] [--no-devtools] + genclass-runtime init [--mode observe|guard|heal] [--token | --no-token] [--agent] [--yes] [--dry-run] [--no-install] [--no-devtools] genclass-runtime remove [--yes] [--dry-run] [--keep-package] genclass-runtime login [--yes] [--no-browser] [--no-connect] genclass-runtime connect [--app ] [--name ] [--yes] [--dry-run] [--no-install] diff --git a/packages/runtime/bin/lib/edit.mjs b/packages/runtime/bin/lib/edit.mjs index 94e21997..90ed7542 100644 --- a/packages/runtime/bin/lib/edit.mjs +++ b/packages/runtime/bin/lib/edit.mjs @@ -119,6 +119,8 @@ export const AUTO = (mode) => (mode ? `@genclass/runtime/auto/${mode}` : "@gencl const AUTO_SPEC = String.raw`"@genclass/runtime/auto(?:/(?:observe|guard|heal))?"`; const DEVTOOLS_CALL = String.raw`import\("@genclass/runtime/devtools"\)\.then\(d=>d\.mountDevtools\(genclass\)\)`; +// the dev-only agent link (init --agent): the same shape, @genclass/runtime/agent's connectAgent on the runtime +const AGENT_CALL = String.raw`import\("@genclass/runtime/agent"\)\.then\(a=>a\.connectAgent\(genclass\)\)`; const COND = String.raw`[^;{}]+?`; const IMPORT_AUTO = String.raw`import(?:genclassfrom)?${AUTO_SPEC};?`; // the options file init writes with --no-telemetry / --telemetry / --model-url, imported right before /auto @@ -126,8 +128,12 @@ const IMPORT_CONFIG = String.raw`import"(?:\.{1,2}/)+(?:[\w.-]+/)*genclass\.conf const CONFIG_STATEMENT = String.raw`(?:\(globalThisas\{GENCLASS_CONFIG\?:\w+\}\)|globalThis)\.GENCLASS_CONFIG=\{[^;]*\};?`; const META_CONFIG = String.raw``; const DEV_LINE = String.raw`if\(${COND}\)${DEVTOOLS_CALL};?`; -// the same with braces (ESLint's `curly` fix) -const DEV_BLOCK = String.raw`if\(${COND}\)\{${DEVTOOLS_CALL};?\}`; +const AGENT_LINE = String.raw`if\(${COND}\)${AGENT_CALL};?`; +// the same with braces (ESLint's `curly` fix), and the one block Next.js's created files hold both calls in +const DEV_BLOCK = String.raw`if\(${COND}\)\{(?:${DEVTOOLS_CALL};?)?(?:${AGENT_CALL};?)?\}`; +const AGENT_BLOCK = String.raw`if\(${COND}\)\{${AGENT_CALL};?\}`; +// plain HTML with --agent: a module script after the runtime's tag (module scripts run after deferred ones, in order) +const AGENT_SCRIPT = String.raw`if\(\["localhost","127\.0\.0\.1"\]\.includes\(location\.hostname\)\)import\("[^"]*"\)\.then\(a=>a\.connectAgent\(GenClass\.runtime\)\);?`; /** Statements init writes on one marked line (normalised, see norm()). */ const LINE_RE = new RegExp( "^(?:" + @@ -137,11 +143,13 @@ const LINE_RE = new RegExp( META_CONFIG, DEV_LINE, DEV_BLOCK, + AGENT_LINE, + AGENT_SCRIPT, String.raw`import\{isDevModeasgenclassDevMode\}from"@angular/core";?`, String.raw`import\{GenClassInit\}from"[^"]*genclass-init";?`, String.raw``, String.raw`]*>`, - String.raw``, + String.raw``, ].join("|") + ")$", ); @@ -155,20 +163,23 @@ const BLOCK_RE = new RegExp( CONFIG_STATEMENT, DEV_LINE, DEV_BLOCK, + AGENT_LINE, String.raw`importtype\{AppProps\}from"next/app";?`, String.raw`exportfunctionGenClassInit\(\)\{returnnull;?\}`, - String.raw`exportdefaultdefineNuxtPlugin\(\(\)=>\{(?:${DEV_LINE}|${DEV_BLOCK})?\}\);?`, + String.raw`exportdefaultdefineNuxtPlugin\(\(\)=>\{(?:${DEV_LINE}|${DEV_BLOCK})?(?:${AGENT_LINE}|${AGENT_BLOCK})?\}\);?`, String.raw`exportdefaultfunctionApp\(\{Component,pageProps\}(?::AppProps)?\)\{return\(?\)?;?\}`, ].join("|") + ")*$", ); // Inline forms (init puts them on a line shared with other code); whitespace a formatter adds between the element and // its marker is taken out with them. `ok` checks the element is still what init wrote (an Astro $`); +const INLINE_SCRIPT_RE = new RegExp(String.raw`^$`); +const AGENT_SCRIPT_RE = new RegExp(`^${AGENT_SCRIPT}$`); const INLINE = [ { re: /\s*\{\s*\/\*\s*genclass:inline\s*\*\/\s*\}/g }, { re: /`; +} const unpkgUrl = (version, min) => `https://unpkg.com/@genclass/runtime@${version}/dist/genclass.global${min ? ".min" : ""}.js`; /** @@ -305,6 +328,7 @@ function planHtml(p, o, ctx) { const files = htmlFiles(p.dir); if (!files.length) return { changes: [], manual: "no .html file with a in this directory" }; const tag = scriptTag(o, ctx.pkgDir, ctx.version); + const agent = o.agent ? agentTag(o, ctx.version) : null; const changes = []; for (const f of files) { const before = readText(f) ?? ""; @@ -313,14 +337,15 @@ function planHtml(p, o, ctx) { const line = lines[li]; let after; if (/<\/head>/i.test(line)) { - // a one-line head: put the tag right after + // a one-line head: put the tag (and the agent script) right after const m = /]*>/i.exec(before); const at = m.index + m[0].length; - after = before.slice(0, at) + `${tag}` + before.slice(at); + after = before.slice(0, at) + `${tag}` + (agent ? `${agent}` : "") + before.slice(at); } else { const next = lines.slice(li + 1).find((l) => l.trim() !== ""); const indent = next !== undefined ? indentOf(next) : indentOf(line) + indentUnit(before); after = insertLineAt(before, li + 1, `${indent}${tag} `); + if (agent) after = insertLineAt(after, li + 2, `${indent}${agent} `); } changes.push({ file: f, kind: "modify", before, after }); } diff --git a/packages/runtime/test/install/cli.test.ts b/packages/runtime/test/install/cli.test.ts index b149f9c4..63c0f5ec 100644 --- a/packages/runtime/test/install/cli.test.ts +++ b/packages/runtime/test/install/cli.test.ts @@ -1228,3 +1228,88 @@ describe("the newest line: version line, npm dist-tags, older runtimes upgraded expect(r.out).toContain(`installs @genclass/runtime (this version, ${VERSION}; an older one already in the`); }); }); + +describe("init --agent: the dev-only agent link next to the overlay (the open-source setup's app side)", () => { + const VITE_TS = { + "package.json": pkg({ react: "^19.0.0", vite: "^7.0.0", "@vitejs/plugin-react": "^5" }), + "tsconfig.json": "{}\n", + "index.html": VITE_HTML, + "src/main.tsx": `import { createRoot } from 'react-dom/client'\nimport App from './App.tsx'\n\ncreateRoot(document.getElementById('root')!).render()\n`, + "src/App.tsx": `export default function App() { return }\n`, + }; + + it("Vite: the overlay line, then the agent line, both marked; the top import binds the runtime", () => { + const dir = project(VITE_TS); + const r = cli(dir, "init", "--yes", "--no-install", "--agent"); + expect(r.code, r.out).toBe(0); + expect(r.out).toContain("npx -y @genclass/mcp init"); + expect(r.out).toContain("127.0.0.1:47357"); + const main = readFileSync(join(dir, "src/main.tsx"), "utf8"); + const lines = main.trimEnd().split("\n"); + expect(lines[0]).toBe(`import genclass from '@genclass/runtime/auto' // genclass:init`); + expect(lines.at(-2)).toBe(`if (import.meta.env.DEV) import('@genclass/runtime/devtools').then((d) => d.mountDevtools(genclass)) // genclass:init`); + expect(lines.at(-1)).toBe(`if (import.meta.env.DEV) import('@genclass/runtime/agent').then((a) => a.connectAgent(genclass)) // genclass:init`); + expect(cli(dir, "remove", "--yes").code).toBe(0); + expect(readFileSync(join(dir, "src/main.tsx"), "utf8")).toBe(VITE_TS["src/main.tsx"]); + }); + + it("--agent --no-devtools: the runtime is still bound; only the agent line follows", () => { + const after = roundTrip(project(VITE_TS), "--agent", "--no-devtools"); + const lines = after["src/main.tsx"].trimEnd().split("\n"); + expect(lines[0]).toBe(`import genclass from '@genclass/runtime/auto' // genclass:init`); + expect(lines.at(-1)).toBe(`if (import.meta.env.DEV) import('@genclass/runtime/agent').then((a) => a.connectAgent(genclass)) // genclass:init`); + expect(after["src/main.tsx"]).not.toContain("devtools"); + }); + + it("Next.js 16: both calls in the one development block of instrumentation-client.ts", () => { + const dir = project({ + "package.json": pkg({ next: "16.0.0", react: "^19" }), + "tsconfig.json": "{}\n", + "node_modules/next/package.json": JSON.stringify({ name: "next", version: "16.0.1" }), + "src/app/layout.tsx": `export default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n \n {children}\n \n );\n}\n`, + }); + const f = roundTrip(dir, "--agent")["src/instrumentation-client.ts"]; + expect(f).toContain(`if (process.env.NODE_ENV === "development") {\n import("@genclass/runtime/devtools").then((d) => d.mountDevtools(genclass));\n import("@genclass/runtime/agent").then((a) => a.connectAgent(genclass));\n}`); + }); + + it("Nuxt 4 and Astro: the agent line in the plugin, and in the inline script", () => { + const n4 = project({ "package.json": pkg({ nuxt: "^4.0.0" }), "tsconfig.json": "{}\n", "app/app.vue": "\n" }); + const plugin = roundTrip(n4, "--agent")["app/plugins/genclass.client.ts"]; + expect(plugin).toContain(` if (import.meta.dev) import("@genclass/runtime/devtools").then((d) => d.mountDevtools(genclass));\n if (import.meta.dev) import("@genclass/runtime/agent").then((a) => a.connectAgent(genclass));\n});`); + const astro = project({ "package.json": pkg({ astro: "^5" }), "src/pages/index.astro": `xhi\n` }); + expect(roundTrip(astro, "--agent")["src/pages/index.astro"]).toContain( + ``, + ); + }); + + it("plain HTML: a module script after the tag, loading the agent entry next to the script URL", () => { + const dir = project({ + "index.html": `\n\n\n \n\n\n\n`, + "about.html": `About`, + }); + const after = roundTrip(dir, "--agent", "--cdn", "https://example.test/dist/genclass.global.min.js", "--no-sri"); + const agent = ``; + expect(after["index.html"]).toContain(`\n \n ${agent} \n `); + expect(after["about.html"]).toContain(`>${agent}`); + const dflt = roundTrip(project({ "index.html": `<html><head><title>x\n` }), "--agent")["index.html"]; + expect(dflt).toContain(`import("https://cdn.jsdelivr.net/npm/@genclass/runtime@${VERSION}/dist/agent.js")`); + }); + + it("remove takes out a Prettier-wrapped agent line whole, and refuses one that was edited", () => { + const dir = viteVue(); + const before = snapshot(dir); + expect(cli(dir, "init", "--yes", "--no-install", "--agent").code).toBe(0); + const f = join(dir, "src/main.js"); + const added = readFileSync(f, "utf8"); + const line = `if (import.meta.env.DEV) import("@genclass/runtime/agent").then((a) => a.connectAgent(genclass)); // genclass:init`; + expect(added).toContain(line); + writeFileSync(f, added.replace(line, `if (import.meta.env.DEV)\n import("@genclass/runtime/agent").then((a) =>\n a.connectAgent(genclass),\n ); // genclass:init`)); + expect(cli(dir, "remove", "--yes").code).toBe(0); + expect(snapshot(dir)).toEqual(before); + expect(cli(dir, "init", "--yes", "--no-install", "--agent").code).toBe(0); + writeFileSync(f, readFileSync(f, "utf8").replace("a.connectAgent(genclass)", "a.connectAgent(genclass, { anyHost: true })")); + const r = cli(dir, "remove", "--yes"); + expect(r.code).not.toBe(0); + expect(r.out).toContain("not a statement init writes"); + }); +}); diff --git a/telemetry-worker/src/pages.ts b/telemetry-worker/src/pages.ts index 9c55fab7..9ac16baf 100644 --- a/telemetry-worker/src/pages.ts +++ b/telemetry-worker/src/pages.ts @@ -369,10 +369,11 @@ export interface OpenSourceChoices { } /** - * The prompt /start/open-source copies: installPrompt's steps without a token, a dashboard or an agent link, with - * `telemetry: false` (the runtime sends no data anywhere; it still downloads public files from jsDelivr unless the app - * serves them), the MCP server on the developer's machine and the page check run locally. Pure and self-contained, - * like installPrompt: the page script runs this same function. + * The prompt /start/open-source copies: the two commands (`init --no-telemetry --agent` for the app, `@genclass/mcp + * init` for the agent), with `telemetry: false` (the runtime sends no data anywhere; it still downloads public files + * from jsDelivr unless the app serves them), no token, the MCP server on the developer's machine and the page check + * run locally; the choices are init flags. Pure and self-contained, like installPrompt: the page script runs this + * same function. */ export function openSourcePrompt(c: OpenSourceChoices = {}, n: PromptNames = PROMPT_NAMES): string { const guard = !!c.guard; @@ -380,64 +381,40 @@ export function openSourcePrompt(c: OpenSourceChoices = {}, n: PromptNames = PRO const logs = !!c.logs; const own = !!c.selfHost; const dir = "/genclass-model/"; - // GenClass.init has automatic state discovery off unless asked (autoState: true, with the discover import first) - const init = ["telemetry: false", "autoState: true"]; - if (guard) init.push('mode: "guard"', ...(eager ? ['aggressiveness: "eager"'] : []), "visual: { on: true, fix: true }"); - if (own) init.push(`model: { baseUrl: "${dir}", ortWasmPaths: "${dir}ort/" }`); - if (logs) init.push("sinks: [consoleJson()]"); - // the script tag carries all of it but the aggressiveness, which goes in GENCLASS_CONFIG (as in installPrompt) - const attrs = ['data-telemetry="off"']; - if (guard) attrs.push('data-mode="guard"', 'data-visual="fix"'); - if (own) attrs.push(`data-model="${dir}"`, `data-ort="${dir}ort/"`); - if (logs) attrs.push('data-sink="console-json"'); - attrs.push('data-devtools="local"'); - const flags = ["--no-telemetry", ...(guard ? ["--mode guard"] : []), ...(logs ? ["--sink console-json"] : []), ...(own ? [`--model-url ${dir}`] : [])]; - // init writes no visual or aggressiveness option: the prompt says where to add them + // every choice is an init flag, except the visual fixes and the aggressiveness, which init does not write + const flags = ["--no-telemetry", "--agent", ...(guard ? ["--mode guard"] : []), ...(logs ? ["--sink console-json"] : []), ...(own ? [`--model-url ${dir}`] : []), "--yes"]; const initAdds = guard - ? `${eager ? 'aggressiveness: "eager" and ' : ""}visual: { on: true, fix: true } to the option file it writes (data-visual="fix" on the tag it writes${eager ? ", and the GENCLASS_CONFIG line above" : ""}; Astro gets a line instead: add visual=fix to its content${eager ? ', and the aggressiveness in a window.GENCLASS_CONFIG script before it' : ""})` + ? `add ${eager ? 'aggressiveness: "eager" and ' : ""}visual: { on: true, fix: true } to the option file it writes (data-visual="fix" on the tag it writes${eager ? ', and a line before it' : ""}; Astro gets a line instead: add visual=fix to its content${eager ? ", and the aggressiveness in a window.GENCLASS_CONFIG script before it" : ""})` : ""; - const local = '["localhost", "127.0.0.1"].includes(location.hostname)'; // A step is a one-element array, numbered at the end (choices add steps); other lines are strings. No named inner // function: Wrangler's bundler (keepNames) would wrap it in a __name() helper that the page script, which runs this // function's source, lacks. const out: (string | [string])[] = [ - "Add GenClass's open-source parts to this web app (Apache-2.0, https://github.com/genclass-dev/GenClass-lib): the @genclass/runtime library in its pages, which reports the bugs that never throw (double submits, stale responses that overwrite newer data, write races, visual bugs), and the @genclass/mcp server on this machine, so you, the coding agent, can see what happens on the dev pages, replay it and prove fixes. No GenClass account and no token: nothing is sent to genclass.dev. Do every step yourself.", + "Add GenClass's open-source parts to this web app (Apache-2.0, https://github.com/genclass-dev/GenClass): the @genclass/runtime library in its pages, which reports the bugs that never throw (double submits, stale responses that overwrite newer data, write races, visual bugs), and the @genclass/mcp server on this machine, so you, the coding agent, can see what happens on the dev pages, replay it and prove fixes. No GenClass account and no token: nothing is sent to genclass.dev. Two commands do it; run them yourself.", "", - [`Install the runtime with the project's package manager: npm install ${n.pkg} (or pnpm add / yarn add / bun add ${n.pkg}). A site with no build step (plain HTML pages, no bundler) installs nothing: it uses the script tags below.`], + ...(own + ? [ + [`Serve the model from the app itself, so its pages download no model from a CDN: npx ${n.pkg} fetch-model public/genclass-model (the folder the project serves as-is: public/ for Vite, Next.js and Astro, static/ for SvelteKit). It downloads the model and onnxruntime-web's files, checks the model files against the sizes and SHA-256 hashes its model card lists, and records onnxruntime-web's hashes in ort/ort.json; the next step's --model-url points at it. On a host with a 25 MiB per-file limit (Cloudflare), run fetch-model public/genclass-model --variant q8 --ort wasm instead (init --model-url adds device: "wasm" when it finds only the WASM build). With a bundler nothing else loads from a CDN. Without one, the runtime's script and what it loads on demand (its model worker, onnxruntime-web's JavaScript, sinks, visual checks, the overlay, the agent link) still come from jsDelivr unless you serve the package's dist/ folder from the app and point the script tag at it (--cdn ; data-base if they are in different folders).`] as [string], + ] + : []), [ - "Start it as early as possible, with GenClass's anonymous diagnostics off (telemetry: false), so it sends no data anywhere" + + `In the project's root run: npx ${n.pkg} init ${flags.join(" ")} ` + + "It finds the framework and package manager, installs the runtime, adds one marked import first in the browser entry (plain HTML pages, no bundler: the script tag, pinned with an integrity hash, in every page's ), the dev-only devtools overlay and the dev-only agent link (which records what happens on local pages for the GenClass MCP server, talks only to 127.0.0.1 and is never part of a production build), and writes telemetry: false into a marked option file next to the entry (data-telemetry=\"off\" on the tag), so GenClass's anonymous diagnostics are off and it sends no data anywhere" + (own ? "" : "; the browser still downloads its model and onnxruntime-web, public files, from cdn.jsdelivr.net (about 13 MB, 18 MB on WebGPU devices, then cached) unless the app serves them") + - ". With a bundler, in the browser entry (main.ts, the root layout or the root component), with the discover import first, before React, Redux or Zustand load:", + ". It shows the diff; --yes applies it. " + + (guard + ? `Mode guard: when GenClass is sure, it acts with minimal, reversible steps (it drops a stale response or fixes a CSS bug on the page) and never changes a response${eager ? '; aggressiveness "eager" lets it act on smaller signals' : ""}. Then ${initAdds}.` + : "The mode stays observe: GenClass reports and never changes what the app does.") + + ' React state, Redux stores (configureStore, or createStore with the Redux DevTools compose) and Zustand stores made with the devtools middleware are found automatically; Pinia, Svelte stores, signals and a plain page\'s DOM are watched only for state the app registers with GenClass.runtime.atom("name", initial) or GenClass.runtime.guard("name", { get, set }), written through the handle (one atom per response, such as a search\'s query and its results): do that only if I ask.', ], - ' import "@genclass/runtime/discover"; // first: lets GenClass find React, Redux and Zustand state', - ...(logs ? [' import { consoleJson } from "@genclass/runtime/sinks/console-json";'] : []), - ' import { GenClass } from "@genclass/runtime";', - ` const rt = GenClass.init({ ${init.join(", ")} });`, - " Without a bundler, in the of every HTML page, before the page's own scripts:", - ...(eager ? [' '] : []), - ` `, - " and in the page's scripts the handle (rt below) is GenClass.runtime.", - ` (Or npx ${n.pkg} init ${flags.join(" ")} --yes does steps 1 and 2 and shows the diff, with a bundler or plain HTML pages: it starts GenClass with its zero-code entry (the /auto import with an option file, or the script tag pinned with an integrity hash), which finds React, Redux and Zustand state by itself, and adds step 3's dev-only overlay; the handle is then GenClass.runtime${guard ? `; add ${initAdds}` : ""}${own ? "; run the fetch-model command below before it" : ""}.)`, [ - (guard - ? `Mode guard: when GenClass is sure, it acts with minimal, reversible steps (it drops a stale response or fixes a CSS bug on the page) and never changes a response${eager ? '; aggressiveness "eager" lets it act on smaller signals' : ""}.` - : "Keep the default mode, observe: GenClass reports and never changes what the app does.") + - ' In development builds also mount the devtools overlay, which shows what GenClass sees on the page: if (import.meta.env.DEV) import("@genclass/runtime/devtools").then((d) => d.mountDevtools(rt)); (Next.js: process.env.NODE_ENV === "development"; the script tag\'s data-devtools="local" does the same).', + `Run: npx -y ${n.mcp} init. It writes the GenClass MCP server into .mcp.json for Claude Code (npx -y ${n.mcp} config prints the block for Cursor, VS Code, Codex, Gemini CLI and others) and the genclass skill, the procedure you follow for timing bugs, to .claude/skills/genclass/SKILL.md; then reload your MCP servers. It runs on this machine, its bridge listens on 127.0.0.1 only, and it needs no account: leave GENCLASS_DASHBOARD and GENCLASS_REPORT unset (either one sends to a GenClass dashboard). The app-side lines it prints are already done by the init command above. Its tools: genclass_status, genclass_findings, genclass_explain, genclass_replay, genclass_explore, genclass_scan.`, ], - ['Connect the state that responses write (search results, carts, lists, forms), so GenClass sees each write with the response or click that caused it. React state, Redux stores (configureStore, or createStore with the Redux DevTools compose) and Zustand stores made with the devtools middleware are found automatically (the discover import with autoState: true above; the script tag and init\'s /auto import do it by default); otherwise create it with rt.atom("name", initial) or wrap the store you have with rt.guard("name", { get, set }), and write through the handle. Values one response writes together (a search\'s query and its results) go in one atom, written once per response, so GenClass can drop a stale response as one write: const search = rt.atom("search", { query: "", results: [] }); then search.set((s) => ({ ...s, query: data.q, results: data.results })) and render from search.subscribe.'], - ['Development builds only, add the agent link: import { connectAgent } from "@genclass/runtime/agent"; if (import.meta.env.DEV) connectAgent(rt); (Next.js: process.env.NODE_ENV === "development"). It records what happens on local pages for the GenClass MCP server, talks only to 127.0.0.1 and is never part of a production build.'], - ` Without a bundler, after the runtime's script tag: `, - [`Add the GenClass MCP server to yourself: npx -y ${n.mcp} init writes .mcp.json for Claude Code (npx -y ${n.mcp} config prints the block for Cursor, VS Code, Codex, Gemini CLI and others); then reload your MCP servers. It runs on this machine, its bridge listens on 127.0.0.1 only, and it needs no account: leave GENCLASS_DASHBOARD and GENCLASS_REPORT unset (either one sends to a GenClass dashboard). init also prints setup lines for the app with a bare GenClass.init(): keep the setup from steps 2 and 5 instead. Its tools: genclass_status, genclass_findings, genclass_explain, genclass_replay, genclass_explore, genclass_scan.`], ...(logs ? [ ['Findings print as [GenClass] lines in the browser console, and the console-json sink also prints each one as one JSON line, for the log shipper the app already uses. If the team has its own endpoint for them, the webhook sink sends batched POSTs there instead: import { webhook } from "@genclass/runtime/sinks/webhook"; sinks: [webhook({ url: "" })], requests: { ignore: [""] } (so GenClass does not watch its own POSTs; data-sink="webhook:" on the script tag does both). Ask me for that URL rather than inventing one.'] as [string], ] : []), - ...(own - ? [ - [`Serve the model from the app itself, so its pages download no model from a CDN: npx ${n.pkg} fetch-model public/genclass-model (the folder the project serves as-is: public/ for Vite, Next.js and Astro, static/ for SvelteKit), before init if you use init. It downloads the model and onnxruntime-web's files, checks the model files against the sizes and SHA-256 hashes its model card lists, and records onnxruntime-web's hashes in ort/ort.json; the model option above points at it. On a host with a 25 MiB per-file limit (Cloudflare), run fetch-model public/genclass-model --variant q8 --ort wasm instead and add device: "wasm" to the model option (data-device="wasm" on the script tag; init --model-url adds it when it finds only the WASM build). With a bundler nothing else loads from a CDN. Without one, the runtime's script and what it loads on demand (its model worker, onnxruntime-web's JavaScript, sinks, visual checks, the overlay, the agent link) still come from jsDelivr unless you serve the package's dist/ folder from the app and point the script tag and the agent link's import at it (data-base if they are in different folders).`] as [string], - ] - : []), [`Run the dev server and load a page to check that the app builds and runs without errors. Until the MCP server runs (your next conversation), the page logs a failed request to 127.0.0.1:47357: that is the agent link finding no server, and expected. In your next conversation, reload a dev page and genclass_status lists it. Then check a page for visual bugs with genclass_scan on its local URL, or from a terminal: npx ${n.pkg} scan http://localhost:5173/ --local (keep --local). It runs headless Chromium on this machine and needs Playwright: if the scan says it is missing, ask me before adding playwright as a dev dependency and downloading Chromium (about 600 MB): npm i -D playwright && npx playwright install chromium; or skip the scan and say so. Do not use hosted: true: it sends the page URL to GenClass's hosted page check, which is not open source.`], ...(c.deploy ? [["Deploy it, as chosen, with the command this project already uses (a deploy script in package.json, wrangler deploy for a Cloudflare Worker, vercel deploy --prod, and so on), then check that the deployed site serves the change."] as [string]] @@ -462,11 +439,6 @@ function choiceOpt(k: string, label: string, note: string, sub = false): string return ``; } -/** The commands' agent link, before its development check (which is the bundler's: init writes the same one). */ -const AGENT_LINK_HEAD = `// in the app's entry, after the lines init added -import { GenClass } from "@genclass/runtime"; -import { connectAgent } from "@genclass/runtime/agent";`; - /** The prompt block's styles on genclass.dev's palette (the dashboard's .copts and .cprompt, DASH_CSS, re-tokened). */ const OSS_CSS = ` .copts{display:grid;gap:6px;max-width:72ch} @@ -517,7 +489,7 @@ export function openSourcePage(nonce: string): string {
-

Paste it into Claude Code, Cursor or Codex: it adds GenClass to your app and the MCP server to the agent, with nothing to sign up for.

+

Paste it into Claude Code, Cursor or Codex: two commands add GenClass to your app and the MCP server and the genclass skill to the agent, with nothing to sign up for.

@@ -533,12 +505,10 @@ export function openSourcePage(nonce: string): string {

Or run the commands yourself

- ${cmd("Add the runtime to your app", `npx ${RUNTIME_PKG} init --no-telemetry`)} - ${cmd("Connect the dev pages to the MCP server: Vite", `${AGENT_LINK_HEAD}\nif (import.meta.env.DEV && GenClass.runtime) connectAgent(GenClass.runtime);`)} - ${cmd("The same for Next.js and webpack", `${AGENT_LINK_HEAD.replace("the app's entry", "the app's entry (Next.js: instrumentation-client.ts)")}\nif (process.env.NODE_ENV === "development" && GenClass.runtime) connectAgent(GenClass.runtime);`)} - ${cmd("Add the MCP server for Claude Code", `npx -y ${MCP_PKG} init`)} + ${cmd("Add the runtime to your app, with the dev-only agent link", `npx ${RUNTIME_PKG} init --no-telemetry --agent`)} + ${cmd("Add the MCP server and the genclass skill for Claude Code", `npx -y ${MCP_PKG} init`)} ${cmd("Check a page for visual bugs", `npx ${RUNTIME_PKG} scan http://localhost:5173/ --local`)} -

init shows the diff and asks before it writes; add --mode guard for guard. Other agents: npx -y ${MCP_PKG} config <client> prints the block for Cursor, VS Code, Codex, Gemini CLI and others; keep GENCLASS_DASHBOARD and GENCLASS_REPORT out of it. The scan needs Playwright (npm i -D playwright && npx playwright install chromium).

+

init shows the diff and asks before it writes; add --mode guard for guard. Until the MCP server runs, a dev page logs one failed request to 127.0.0.1:47357 (the agent link finding no server). Other agents: npx -y ${MCP_PKG} config <client> prints the block for Cursor, VS Code, Codex, Gemini CLI and others; keep GENCLASS_DASHBOARD and GENCLASS_REPORT out of it. The scan needs Playwright (npm i -D playwright && npx playwright install chromium).