From 2c63e9fb7b7510310cc16f97eae11eb48f3aeb6d Mon Sep 17 00:00:00 2001 From: grjte Date: Wed, 16 Sep 2026 11:31:18 +0100 Subject: [PATCH] feat: add llm-host to manage llm worker provider/client + llm config (moved from chat) --- llm-host/.gitignore | 3 + llm-host/README.md | 361 ++++++ llm-host/client.js | 301 +++++ llm-host/package.json | 34 + llm-host/pnpm-lock.yaml | 2072 ++++++++++++++++++++++++++++++ llm-host/pnpm-workspace.yaml | 11 + llm-host/src/LlmConfigTray.tsx | 216 ++++ llm-host/src/client.js | 106 ++ llm-host/src/client.test.js | 112 ++ llm-host/src/index.ts | 81 ++ llm-host/src/worker-spec.js | 377 ++++++ llm-host/src/worker-spec.test.js | 143 +++ llm-host/tsconfig.json | 15 + llm-host/vite.config.ts | 26 + llm-host/vitest.config.ts | 14 + 15 files changed, 3872 insertions(+) create mode 100644 llm-host/.gitignore create mode 100644 llm-host/README.md create mode 100644 llm-host/client.js create mode 100644 llm-host/package.json create mode 100644 llm-host/pnpm-lock.yaml create mode 100644 llm-host/pnpm-workspace.yaml create mode 100644 llm-host/src/LlmConfigTray.tsx create mode 100644 llm-host/src/client.js create mode 100644 llm-host/src/client.test.js create mode 100644 llm-host/src/index.ts create mode 100644 llm-host/src/worker-spec.js create mode 100644 llm-host/src/worker-spec.test.js create mode 100644 llm-host/tsconfig.json create mode 100644 llm-host/vite.config.ts create mode 100644 llm-host/vitest.config.ts diff --git a/llm-host/.gitignore b/llm-host/.gitignore new file mode 100644 index 00000000..96f32a31 --- /dev/null +++ b/llm-host/.gitignore @@ -0,0 +1,3 @@ +node_modules +dist +.pushwork diff --git a/llm-host/README.md b/llm-host/README.md new file mode 100644 index 00000000..1903ca3c --- /dev/null +++ b/llm-host/README.md @@ -0,0 +1,361 @@ +# @grjte/llm-host + +The **host-realm LLM package**. It is the Patchwork side of the LLM: everything +that must run in a real-origin host realm because it touches the settings doc and +the API key. It registers two plugins: + +- **`llm-config-tray`** (`patchwork:component`, tag `system-tray`) — the settings + doc + the model/config picker UI, so no other tool needs to read or write LLM + config. Renders a small icon in the frame's system tray; clicking it, or a + `patchwork:open-tool` event, opens the picker in a popover. +- **`llm`** (`patchwork:worker`) — the LLM worker itself. Its `load()` resolves a + **WorkerSpec** (via `makeLLMWorkerSpec`) that `@grjte/patchwork-worker` serves, so + a consumer reaches it by `connectWorker("llm", …)` — in or out of isolation. + +The design goal, and the reason this package exists as host-realm code: the LLM +worker (and the config + API key it needs) runs in the **host realm**, while a +consumer tool like chat holds only streams — the same code whether or not it runs +inside a sandboxed iframe. + +This README covers both the config tray (below) and the full LLM architecture (from +"Three layers" onward). The **generic** worker machinery it rides on — the +`WorkerSpec` contract, `connectWorker` / `serveWorkerSpec`, discovery and the +transferable-stream handoff, one-worker-per-connection — is documented in +[`../libraries/patchwork-worker/README.md`](../libraries/patchwork-worker/README.md). This package adds +the LLM-specific parts: the op vocabulary, the config/secret trust boundary, and the +config tray. + +--- + +## The config tray (`llm-config-tray`) + +### What it owns + +- **The settings doc.** LLM config lives in a dedicated Automerge document whose + body *is* the config (`provider`, per-provider `model`/`apiKey`/`url`, sampling + params, `toolToggles`, prompt-doc refs, and per-tool/per-doc overrides). The + account doc only holds a *pointer* to it (`accountDoc.toolStorage["llm"]`), + resolved via the `patchwork:tool-storage` provider. The only secret is + `openrouter.apiKey`, stored here and nowhere else. +- **The picker UI.** It reuses `@chee/patchwork-llm`'s `dom()` picker, driven + through the picker's injectable `source` `{read, write}` — backed by + `readScopedConfig` / `writeScopeOverride` / `writeConfig`. So this tool is the + sole writer of the settings doc; the picker never touches the doc singletons + directly. + +### How it's opened + +- **Sidebar icon:** the tray button toggles the popover. +- **`patchwork:open-tool` event:** any tool can dispatch + `new CustomEvent("patchwork:open-tool", { detail: { component: "llm-config-tray", + scope, toolPrompt, toolTools }, bubbles: true, composed: true })`. The tray has a + document-level listener that opens the picker for that `scope` and surfaces the + caller's `toolPrompt` / `toolTools` (tools tagged `defaultOff` render unchecked). + This is how chat's `/model` opens it — and, when chat runs inside the isolation + sandbox, the isolation *open-tool bridge* relays the event to the host so this + host-realm tool opens there. + +### Why host-only + +The settings doc (with the API key) is denylisted from the isolation sandbox, and +resolving it needs `window.repo` + a mounted ``. So config +management can only run in the host realm. This tool is mounted by the frame as host +chrome; consumer tools stay config-free and reach it via the event above. + +--- + +## The packages + +``` + @grjte/patchwork-worker the generic transport (connect / session / serve halves), a + plain library. Knows nothing about LLMs. See its README. The + host-realm provider that drives it (patchwork-worker-provider) + ships from the `providers` package. The LLM uses it unchanged: + it registers a WorkerSpec and connects by the id "llm". + + @chee/patchwork-llm the LLM library. Owns worker.js (transformers.js / OpenRouter / + Ollama), config resolution over the settings doc (config.js: + ensureConfig, callConfig, the apiKey), the picker UI, tool + parsing, request preparation (request.js: prepareGenerate + + buildGeneratePayload, shared by its own generate() and by + worker-spec.js below, so the two paths cannot diverge), and + `createWorker()` — the ONE worker-transport + concern that must live here, because + `new URL("./worker.js", import.meta.url)` has to resolve + against this library's own (cross-origin) URL, and bundlers + only emit the worker chunk when they see that exact literal. + NOTE: this is a Patchwork-coupled library, not a plain npm + one — it imports @inkandswitch/patchwork-{elements,providers}, + reads the account/settings docs, and ships a + element. It has no `plugins` + array, so importing it registers nothing; llm-host is what + makes it a Patchwork worker. + + @grjte/llm-host THIS package — the host-realm glue. Registers THREE plugins: + llm-config-tray (component), llm (patchwork:worker) and llm + (patchwork:worker-client). Owns BOTH halves of the LLM + protocol: src/worker-spec.js (serve half: host policy — which + overrides a consumer may set, system maps, toolToggles, + host-side parsing — over the library's request + preparation) and src/client.js (consume half: makeLLMClient, + pure, delivered to consumers THROUGH THE REGISTRY as a lazy + dist chunk). The library is imported lazily, so nothing + LLM-specific loads until first connect. + + chat (@patchwork/chat*) the CONSUMER. Imports only the transport + (@grjte/patchwork-worker, a library); has NO dependency on + this package. Calls connectWorkerClient("llm") and receives + makeLLMClient's {generate} at runtime; runs no worker; opens + config via event. Reads ZERO config. + + @patchwork/isolation the sandbox. When chat runs inside it, three relays cross the + boundary: worker-channel (streams), open-tool (config picker), + and the registry source-rewrite (so tool code + its automerge + deps load). None of it is LLM-specific. + + threepane the host frame. Mounts patchwork-worker-provider + the config + tray; sets shared-providers on the isolation element. +``` + +## Three layers, each kind-agnostic below the one above + +``` + layer package serve half consume half + ----- ------- ---------- ------------ + transport patchwork-worker serveWorkerSpec openSession / connectWorkerClient + protocol llm-host src/worker-spec.js src/client.js (makeLLMClient) + compute @chee/patchwork-llm worker.js createWorker +``` + +The transport knows nothing about the LLM's op vocabulary; the protocol layer knows +nothing about streams or ids. Both halves of the **LLM protocol** live in this +package and are registered as a PAIR of plugins with the same id, `"llm"`: +`patchwork:worker` resolves to the WorkerSpec (`src/worker-spec.js`, served by the +host provider) and `patchwork:worker-client` resolves to `makeLLMClient` +(`src/client.js`, a factory `(session) => {generate}`). The transport defines that +convention and does the lookup: a tool calls `connectWorkerClient("llm", …)`, which +finds the client plugin in the registry, opens a session for `"llm"`, and returns +`makeLLMClient(session)`. The tool never names this package at build time; the two +halves ship in one doc and cannot drift. `src/client.js` is PURE (imports nothing) +and must stay so — it is the chunk a sandboxed tool loads. A second worker kind +(e.g. transcription) would register its own pair and reuse the transport unchanged. + +## Config ownership — the LLM trust boundary + +This is the part the generic transport can't provide, and the reason llm-host exists +as a host-realm package. + +``` + CONFIG OWNERSHIP CONFIG RESOLUTION + WORKER CONSUMER + (llm-host, host) (llm-host worker-spec, host) (chat) + ------------------ --------------------------- -------- + settings doc ensureConfig(scope) -> CallConfig names a scope + picker UI (config tray) drives worker.js via WorkerSpec writes request frames + apiKey lives here apiKey injected here, never out reads event streams + reads ZERO config +``` + +The consumer never touches config. Everything config-derived is either resolved +host-side from the `scope` chat sends, or rides back on the generation stream (e.g. +the model label). So chat imports nothing from the LLM library, and no secret is +ever reachable from the sandbox. + +`worker-spec.js`'s `resolveForFrame` is where this is enforced: config is resolved +from the frame's `scope` alone; a consumer-supplied `frame.config` is ignored (it +would let a tool pick the provider AND supply its own key, defeating host-side +resolution). The apiKey enters the worker call host-side via `callConfig` and is +never placed in a request or event frame. + +## The LLM op vocabulary + +The transport treats frames as opaque (it reads only `id` and the reserved +`op:"abort"`). The LLM defines this vocabulary on top: + +``` + request (consumer -> worker): {id, op:"generate"|"predict"|"preload"| + "score-tokens"|... , scope, system, tools, + messages|text, sessionKey, ...sampling knobs} + events (worker -> consumer): {id, type:"token"|"stats"|"prediction"|"status"| + "model"|"result"|"error", ...} + result / error are terminal; abort = readable.cancel() +``` + +`worker-spec.js`'s `handle` switches on `op`: `generate` (incl. a `builtin` branch +for the Chrome Prompt API, which runs in-realm with no worker), `predict`, the +analytical ops (`score-tokens`, `compute-importance`, …), and `preload`. The consume +half (`client.js`) handles `token` / `status` / `model` progress plus the `result` / +`error` terminals. These two lists are maintained by hand and must stay in step — a +mismatch is silent (a frame type the serve half emits that the consume half doesn't +handle is simply ignored). + +## The two topologies + +The generic mechanics of each — who answers the subscription, how streams are +transferred within a realm vs. across the boundary — are in the worker README. What +follows is only the LLM-specific detail. + +### Topology A — isolation OFF (chat runs in the host realm) + +Everything is one realm. chat calls `connectWorkerClient("llm")`: the host registry +resolves the `patchwork:worker-client` plugin (this package's entry, then the +`client-*.js` chunk), and the returned client opens a session on first generate. The +`patchwork-worker-provider` (an ancestor in the frame) answers directly and hands chat +the streams by transfer. The worker runs right there, in the host origin (model +cache, WebGPU, `fetch`). + +``` + chat worker-provider llm-host worker-spec worker.js + ---- --------------- ------------------- --------- + connect ──────► answer subscribe ───────► serveWorkerSpec(spec) + spec.open: ensureConfig ──► (reads settings doc) + (settings warm) + ◄── {readable,writable} (transfer) ── + write {op:generate,scope,system,tools,messages} ─────► spec.handle: callConfig(+apiKey) + post to worker ─────────► token / stats / model / + read {type:model,label} ◄── result / error + read {type:token,...} ◄── (stream) + read {type:result,...} ◄── (terminal) +``` + +Opening the config picker: chat dispatches `patchwork:open-tool +{component:"llm-config-tray", scope, ...}`; the tray's document listener opens the +picker. No bridge involved. + +### Topology B — isolation ON (chat runs inside the sandboxed iframe) + +Same chat code, same `connectWorkerClient("llm")` call. The plugin registry is +mirrored into the iframe (every registry type, ungated — entries arrive with the boot +snapshot and as live `plugin-registered` pushes, and `loadWhenReady` waits for a late +one), so the lookup resolves in-sandbox: the iframe module loader fetches +`registry--@grjte--llm-host/dist/index.js` (descriptors only) and then the +`client-*.js` chunk — the ONLY llm-host code that enters the sandbox. The worker +can't run in the opaque-origin iframe (no shared model cache; the settings doc / API +key are denylisted), so the host answers and the streams are transferred **across +the boundary**. The three relays are generic (see ISOLATION.md); the LLM-specific +facts are: + +- **Why the host must answer.** The two reasons are both LLM-shaped: the model cache + / WebGPU device live in the host origin, and the settings doc + API key are + host-only. A worker kind with neither constraint could in principle run + in-boundary — the LLM cannot. +- **What crosses:** only the stream pair (plus the small setup messages). After the + handoff, tokens flow iframe⇄host-worker directly over the transferred port; the + bridge is involved only in *setup*. The API key never crosses — it is applied + host-side inside `worker-spec.js`. +- **Opt-in:** `patchwork:worker-channel` in `shared-providers` enables the worker + handoff, and `llm-config-tray` in `shared-tools` enables the open-tool relay for + the picker. Without the first, the session's subscription is never claimed, + `connectWorker` resolves null after its discovery backstop, and chat degrades. + +``` + chat (iframe) iframe providers-bridge host providers-bridge worker-provider / spec + ------------- ----------------------- --------------------- ---------------------- + connect ─────────► capture unclaimed sub + (subscribe) RPC {providers-bridge, ──────────────────────► gate selector.type ∈ + id, selector} shared-providers + re-dispatch subscribe on host + ◄ provider serves spec: + ensureConfig(scope)+worker + ◄ {type:"change", value:{readable,writable}} + (transfer, host port) + ◄ RPC {providers-bridge-change, + id, value} (TRANSFER across boundary) + ◄ value handed to chat's session + write {op:generate,...} ─────────────────────────────────────────► worker.js (host origin) + read token/model/result ◄──────────────────── (streams flow iframe⇄host directly now) +``` + +Opening the config picker (isolation on): chat dispatches the same +`patchwork:open-tool`; the iframe forwards it over RPC, the host open-tool bridge +gates it on `component ∈ ALLOWED_OPEN_TOOLS` (`["llm-config-tray"]`), and +re-dispatches on the host element so the tray opens **in the host realm**. The picker +— with the settings doc + API key — always runs in the host; the iframe only +*triggers* it. + +## Why @chee/patchwork-llm is NOT loaded in the iframe + +chat imports only the transport (`@grjte/patchwork-worker/client.js`, a library +subpath baked by `@chee/patchwork-bundles`). What it then loads from THIS package — +through the registry, not an import — is `dist/index.js` (three plugin descriptors, +no top-level imports) and the `client-*.js` chunk (imports nothing). Neither pulls +in the LLM library. `patchwork-llm-*.js` and `worker-spec-*.js` are reachable only +from the `patchwork:worker` plugin's `load()`, which only the host-realm provider +ever calls. So the transformers.js runtime, the config code, and the API key never +enter the sandbox graph. Two invariants keep it that way: `src/client.js` must never +import `@chee/patchwork-llm` or `./worker-spec.js` (vite would pull the library chunk +into the client chunk's graph), and `src/index.ts` must keep everything behind +`load()`. + +(The isolation registry marker is per package, so a sandboxed tool *could* fetch the +other chunks deliberately — as it could the old raw subpath. Nothing secret is in +the served files: the API key lives in the settings doc, which is denylisted.) + +(How tool code and its automerge deps load in the iframe at all — the `registry--` +marker rewrite of the baked `automerge:` dep — is generic isolation machinery; see +ISOLATION.md, "Package registry in iframe".) + +## Data flow + security summary + +- **API key never leaves the host.** Stored in the settings doc (host-only, + denylisted from the iframe), read only by `worker-spec.js` during `callConfig`, and + injected into the worker call host-side. Never in a request frame (chat sends only + `scope`) nor in any event frame. +- **Config is host-resolved from `scope`.** chat sends `{toolId, docId}`; the host + reads the real provider/model/prompts/toolToggles. chat reads no config; the model + *label* comes back as a `{type:"model"}` event frame. +- **Only streams cross the boundary** (plus the small setup messages). After handoff, + tokens flow iframe⇄host-worker directly over the transferred port. +- **Two opt-ins gate the bridges:** `shared-providers` ∩ `ALLOWED_PROVIDERS` + (includes `patchwork:worker-channel`) and `shared-tools` ∩ `ALLOWED_OPEN_TOOLS` + (open-tool). Neither is on by default. +- **isolation stays generic** — it relays worker-channel, open-tool, and the registry + source-rewrite; it imports nothing LLM-specific. + +## Is this the pattern for other workers? + +Yes, in this shape: **one package registers a `patchwork:worker` / `patchwork:worker-client` +pair under one id.** The serve half is a WorkerSpec; the consume half is a factory +`(session) => api`. Consumers call `connectWorkerClient(kind)` and never import the +package. The transport and the isolation bridges are untouched by a new kind. + +The **library + `-host` split is specific to the LLM**, not part of the pattern. It +exists because `@chee/patchwork-llm` predates the transport and is also used directly +(same realm) by other tools, so its request preparation is shared rather than moved. +A worker built transport-first keeps compute, spec and client in one package. + +## File map + +``` + @grjte/patchwork-worker ............. the generic transport library (see its README) + providers/src/WorkerProvider.ts ..... the host-realm provider that drives it + @chee/patchwork-llm worker.js ...... the compute worker; createWorker() constructs it + @chee/patchwork-llm config.js ...... settings doc + config resolution (ensureConfig, + callConfig, the apiKey), + picker.js (the picker UI) + @chee/patchwork-llm request.js ..... prepareGenerate + buildGeneratePayload: request + preparation shared by generate() and worker-spec + @grjte/patchwork-worker client.js ... connectWorkerClient(kind): registry lookup of the + paired worker-client plugin + openSession(kind) + llm-host/src/worker-spec.js ......... makeLLMWorkerSpec: host policy (sampling-only + overrides, system maps, toolToggles, host-side + parsing) over the library's request + preparation — the SERVE half of the protocol + llm-host/src/client.js .............. makeLLMClient(session) → {generate} — the CONSUME + half (pure; a lazy dist chunk resolved via the + patchwork:worker-client plugin) + llm-host/src/index.ts ............... registers llm-config-tray + the "llm" worker and + worker-client plugins + llm-host/src/LlmConfigTray.tsx ...... host config tray: picker + patchwork:open-tool listener + chat/src/components/ChatRoot.tsx .... connectWorkerClient("llm") → client.generate(); + /model dispatches patchwork:open-tool + isolation/src/bridges/providers-bridge.ts host + iframe halves of the provider relay, + incl. worker-channel and its transferred streams + isolation/src/bridges/open-tool-bridge.ts host + iframe halves of the open-tool relay + isolation/src/bridges/registry-bridge.ts marker mapping + served-source dep rewrite + threepane/src/PatchworkFrame.tsx .... mounts worker-provider + config tray + threepane/src/components/IsolatedDocumentArea.tsx sets shared-providers/shared-tools +``` + +## Build / sync + +Bundled with vite + `@chee/patchwork-bundles` (the `automerge:`→cross-origin rewrite +for its `@chee/patchwork-llm` dep). `pnpm build`, then `pushwork sync`. diff --git a/llm-host/client.js b/llm-host/client.js new file mode 100644 index 00000000..f4f4cb4e --- /dev/null +++ b/llm-host/client.js @@ -0,0 +1,301 @@ +/** + * The LLM consume-half — the mirror of worker-spec.js. + * + * worker-spec.js (the SERVE half) turns a consumer's request frames into worker + * traffic; this file (the CONSUME half) turns a tool's `generate(...)` call into + * those request frames and reads the event stream back. Together they are the LLM + * *protocol* layer, sitting above the generic transport (@grjte/patchwork-worker) + * and below any tool that wants to generate. + * + * ⚠ PURE by design — this file imports NOTHING. The transport is injected: a tool + * opens a session with @grjte/patchwork-worker's `openSession` and passes it to + * `makeGenerate`. That keeps this file a raw sandbox-safe subpath (like the + * transport's connect.js) with no cross-package import to bake, and it mirrors the + * transport's own shape — `createOpenSession(connectWorker)` injects its primitive + * the same way. The tool owns the CONNECTION (which worker kind, when to open); + * this file owns only the PROTOCOL. + * + * Frame vocabulary (must stay in step with worker-spec.js): + * request: {id, op:"generate", scope, system, tools, messages|text, sessionKey} + * events: {id, type:"token"|"stats"|"prediction"|"status"|"model"| + * "result"|"error", ...} result/error terminal. + */ + +/** + * @typedef {Object} GenOpts + * @property {any} [scope] {toolId, docId} — the host resolves config for this scope + * @property {string|Record} [system] string | {default, [provider]} + * @property {any[]} [tools] tool descriptors {name, description, defaultOff?, parameters?} + * @property {string} [sessionKey] + * @property {HTMLElement} [element] a node in the mounted , for discovery + * @property {AbortSignal} [signal] + * @property {(delta:string, full:string)=>void} [onToken] + * @property {(message:string)=>void} [onStatus] + * @property {(label:string)=>void} [onModel] + * + * @typedef {Object} GenResult + * @property {string} text + * @property {any[]|null} toolCalls + * @property {string} [toolMode] + */ + +/** + * Build a `generate` bound to an already-open transport session. The tool opens + * the session (`openSession("llm", {...})` from @grjte/patchwork-worker) and hands + * it in; this file only ever calls `session.request(...)`. + * + * @param {{request: (frame:any, opts:any)=>{promise:Promise}}} session + */ +export function makeGenerate(session) { + /** + * Generate a completion. Streams tokens via opts.onToken; resolves with the + * final text + any structured tool calls. Tool EXECUTION stays in the tool — + * the worker only receives tool descriptors and returns calls to run. + * @param {any[]|string} messages + * @param {GenOpts} [opts] + * @returns {Promise} + */ + return function generate(messages, opts = {}) { + const payload = typeof messages === "string" ? {text: messages} : {messages} + + const {promise} = session.request( + { + op: "generate", + sessionKey: opts.sessionKey, + scope: opts.scope, + system: opts.system, + tools: opts.tools, + ...payload, + }, + { + element: opts.element, + signal: opts.signal, + terminal: { + result: (/** @type {any} */ f) => ({ + text: f.text, + toolCalls: f.toolCalls || null, + toolMode: f.toolMode, + }), + error: (/** @type {any} */ f) => { + throw new Error(f.message) + }, + }, + // prediction / stats frames are ignored by the caller unless it asks. + onFrame: (/** @type {any} */ f) => { + if (f.type === "token") opts.onToken?.(f.delta, f.text) + else if (f.type === "status") opts.onStatus?.(f.message) + else if (f.type === "model") opts.onModel?.(f.label) + }, + } + ) + + return /** @type {Promise} */ (promise) + } +} + +/** + * @typedef {{name: string, args: Record}} ToolCall + */ + +/** + * Extract tool calls from a model's text output, for providers without native + * function calling (local / template). Handles LFM bracket syntax, `` + * XML, and fenced/bare JSON. Pure text — copied from @chee/patchwork-llm's + * tools.js (the repo's standalone-package rule forbids linking, so a copy is the + * sanctioned answer; it lives here, in the protocol layer, once for every LLM + * consumer instead of once per tool). + * + * @param {string} text + * @returns {ToolCall[]} + */ +export function parseToolCalls(text) { + if (!text) return [] + /** @type {ToolCall[]} */ + const calls = [] + const push = (/** @type {any} */ obj) => { + const name = obj?.name || obj?.tool + if (!name) return + let args = obj.arguments ?? obj.args ?? {} + if (typeof args === "string") { + try { + args = JSON.parse(args) + } catch { + args = {} + } + } + calls.push({name, args: args || {}}) + } + const thinkingEnd = text.lastIndexOf("") + const lfmRegionStart = thinkingEnd === -1 ? 0 : thinkingEnd + "".length + const lfmMatch = text + .slice(lfmRegionStart) + .search(/\[\s*[a-zA-Z_][a-zA-Z0-9_]*\s*\(/) + const lfmStart = lfmMatch === -1 ? -1 : lfmRegionStart + lfmMatch + if (lfmStart !== -1) { + let depth = 0 + let quote = "" + let escaped = false + let lfmEnd = -1 + for (let i = lfmStart; i < text.length; i++) { + const ch = text[i] + if (escaped) { + escaped = false + continue + } + if (quote) { + if (ch === "\\") escaped = true + else if (ch === quote) quote = "" + continue + } + if (ch === "'" || ch === '"') { + quote = ch + continue + } + if (ch === "[" || ch === "(" || ch === "{") depth++ + else if (ch === "]" || ch === ")" || ch === "}") { + depth-- + if (depth === 0) { + lfmEnd = i + break + } + } + } + if (lfmEnd !== -1) { + const split = (source = "") => { + const parts = [] + let start = 0 + let nested = 0 + let string = "" + let slash = false + for (let i = 0; i < source.length; i++) { + const ch = source[i] + if (slash) { + slash = false + continue + } + if (string) { + if (ch === "\\") slash = true + else if (ch === string) string = "" + continue + } + if (ch === "'" || ch === '"') string = ch + else if (ch === "[" || ch === "(" || ch === "{") nested++ + else if (ch === "]" || ch === ")" || ch === "}") nested-- + else if (ch === "," && nested === 0) { + parts.push(source.slice(start, i).trim()) + start = i + 1 + } + } + const last = source.slice(start).trim() + if (last) parts.push(last) + return parts + } + const literal = (source = "") => { + let json = "" + for (let i = 0; i < source.length; i++) { + const ch = source[i] + if (ch !== "'" && ch !== '"') { + json += ch + continue + } + let value = "" + for (i++; i < source.length; i++) { + const next = source[i] + if (next === ch) break + if (next !== "\\") { + value += next + continue + } + const escaped = source[++i] + value += + escaped === "n" + ? "\n" + : escaped === "r" + ? "\r" + : escaped === "t" + ? "\t" + : escaped + } + json += JSON.stringify(value) + } + try { + return JSON.parse( + json.replace(/\bTrue\b/g, "true").replace(/\bFalse\b/g, "false").replace(/\bNone\b/g, "null") + ) + } catch { + return source + } + } + for (const expression of split(text.slice(lfmStart + 1, lfmEnd))) { + const match = /^([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([\s\S]*)\)$/.exec(expression) + if (!match) continue + const args = Object.fromEntries([]) + for (const arg of split(match[2])) { + const eq = arg.indexOf("=") + if (eq < 1) continue + const name = arg.slice(0, eq).trim() + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) continue + args[name] = literal(arg.slice(eq + 1).trim()) + } + calls.push({name: match[1], args}) + } + if (calls.length) return calls + } + } + let m + const xml = /\s*([\s\S]*?)\s*<\/tool_call>/g + let sawXml = false + while ((m = xml.exec(text))) { + sawXml = true + try { + push(JSON.parse(m[1].trim())) + } catch {} + } + if (sawXml) return calls + const fence = /```(?:json|tool[_-]call)?\s*([\s\S]*?)```/g + while ((m = fence.exec(text))) { + try { + push(JSON.parse(m[1].trim())) + } catch {} + } + if (calls.length) return calls + // Bare JSON objects — brace-depth-aware scan so nested objects (e.g. + // "arguments": { ... }) are captured whole instead of truncated at the + // first inner `}`. + let depth = 0, + start = -1, + inStr = false, + esc = false + for (let i = 0; i < text.length; i++) { + const ch = text[i] + if (esc) { + esc = false + continue + } + if (ch === "\\" && inStr) { + esc = true + continue + } + if (ch === '"') { + inStr = !inStr + continue + } + if (inStr) continue + if (ch === "{") { + if (depth === 0) start = i + depth++ + } else if (ch === "}") { + depth-- + if (depth === 0 && start >= 0) { + const block = text.slice(start, i + 1) + if (/"(?:name|tool)"/.test(block)) { + try { + push(JSON.parse(block)) + } catch {} + } + start = -1 + } + } + } + return calls +} diff --git a/llm-host/package.json b/llm-host/package.json new file mode 100644 index 00000000..ffc8b1e3 --- /dev/null +++ b/llm-host/package.json @@ -0,0 +1,34 @@ +{ + "name": "@grjte/llm-host", + "version": "0.2.0", + "description": "The host-realm LLM package: owns the settings doc + config picker (llm-config-tray) and registers the LLM worker as a patchwork:worker plugin. The library itself is @chee/patchwork-llm.", + "type": "module", + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "push": "pnpm build && pushwork sync", + "register": "pw-modules add \"$MODULE_SETTINGS_DOC_URL\" \"$(pushwork url)\"", + "test": "vitest run", + "test:watch": "vitest" + }, + "author": "grjte", + "license": "MIT", + "dependencies": { + "@chee/patchwork-llm": "automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey", + "solid-js": "^1.9.11" + }, + "devDependencies": { + "@chee/patchwork-bundles": "^0.2.0", + "@inkandswitch/patchwork-bootloader": "^0.0.3", + "@inkandswitch/patchwork-plugins": "^0.0.5", + "happy-dom": "^15.11.7", + "typescript": "^5.8.3", + "vite": "^5.4.19", + "vite-plugin-solid": "^2.10.0", + "vitest": "^3.2.4" + } +} diff --git a/llm-host/pnpm-lock.yaml b/llm-host/pnpm-lock.yaml new file mode 100644 index 00000000..cd54ddfe --- /dev/null +++ b/llm-host/pnpm-lock.yaml @@ -0,0 +1,2072 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: + pnpm-plugin-patchwork: + specifier: 0.4.1 + version: 0.4.1 + +packages: + + pnpm-plugin-patchwork@0.4.1: + resolution: {integrity: sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==} + +snapshots: + + pnpm-plugin-patchwork@0.4.1: {} + +--- +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@chee/patchwork-llm': + specifier: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey + version: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9(@automerge/automerge-repo@2.6.0-subduction.40) + solid-js: + specifier: ^1.9.11 + version: 1.9.15 + devDependencies: + '@chee/patchwork-bundles': + specifier: ^0.2.0 + version: 0.2.0(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-bootloader': + specifier: ^0.0.3 + version: 0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-plugins': + specifier: ^0.0.5 + version: 0.0.5 + happy-dom: + specifier: ^15.11.7 + version: 15.11.7 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^5.4.19 + version: 5.4.21(@types/node@20.19.43) + vite-plugin-solid: + specifier: ^2.10.0 + version: 2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)) + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7) + +packages: + + '@automerge/automerge-repo-keyhive@0.0.0-alpha.71': + resolution: {integrity: sha512-bDP6QPNTG9eCYVnLW/PuODLFmGrOe4rwaOSAWLgW9VrHg/5aBlP0PmrzeJ58loYvGlqYeN6PWrV+GFDqCPUcug==} + + '@automerge/automerge-repo-network-broadcastchannel@2.5.0': + resolution: {integrity: sha512-57rUTYf0W6kL9DXwjRtyGLqeyz65tDIpiNQdc1nXHnN4VkNMvNo0xzb37nyFKWHEqPWA0DGCcBA8HRlbVHHyHw==} + + '@automerge/automerge-repo-network-messagechannel@2.5.0': + resolution: {integrity: sha512-G7m161tId4djPMzDPlP7nIEsDY7ddbKfKvpJP7kkVqv3OA/Ml8fzyKaU7C1++s/g0FSNCTiN6hOhFCbSkb6/Dw==} + + '@automerge/automerge-repo-network-websocket@2.5.0': + resolution: {integrity: sha512-zrwbkW8hk3fWkXdeaH+YDfGG63WaB9WLfWYyinZx2AQv4mr6D6UhIhviIjweMmRxMLen6NlIcdu3toHV33ZuKg==} + + '@automerge/automerge-repo-network-websocket@2.5.0-alpha.2': + resolution: {integrity: sha512-EtdsSA9PhuiWWbvT36DGylBeGCL7Aop8nSe4FrBYchBCWpqqZOBLdbxD0crwbCUwTBTJy76pXhSHjObQi0y4zQ==} + + '@automerge/automerge-repo-storage-indexeddb@2.5.0': + resolution: {integrity: sha512-7MJYJ5S6K7dHlbvs5/u/v9iexqOeprU/qQonup28r2IoVqwzjuN5ezaoVk6JRBMDI/ZxWfU4rNrqVrVlB49yXA==} + + '@automerge/automerge-repo@2.5.0': + resolution: {integrity: sha512-bdxuMuKmxw0ZjwQXecrIX1VrHXf445bYCftNJJ5vqgGWVvINB5ZKFYAbtgPIyu1Y0TXQKvc6eqESaDeL+g8MmA==} + + '@automerge/automerge-repo@2.5.0-alpha.2': + resolution: {integrity: sha512-Sjl1CaD4IpmboNIG9/EL9Run+AKrL8/1h3Om1NZp9syBnJN8OWjeG4ttpQce6k8HdPyl8sllAHYy8/Q4Ukh39w==} + + '@automerge/automerge-repo@2.6.0-subduction.40': + resolution: {integrity: sha512-9r3GS8FtZKoIJOtJqkQR0ukF7HSu+IyV5Iqf7MP3ELDz/TEaUbZV8ysyl5Ju4fVZjmagI/n3oH2h4ySk4FdEFQ==} + engines: {node: '>=22.13'} + + '@automerge/automerge-subduction@0.16.0': + resolution: {integrity: sha512-BOOn/4p8cF4LhjDUQdgM3qyZFya+M7ZuZywuC99x/kbhk4UK7dw/QcsifcHG5E1TAW4R/K17ZGFUC5nCVUSSWg==} + + '@automerge/automerge@3.2.1': + resolution: {integrity: sha512-xsEZlZe8lWyVCbD7Z7roH4X6ZKlRxMmmumb0M5CuxJWptUEfYsf+1z9lFEJh4aDBbWTLAgxsXd+TBi6jKTj0+g==} + + '@automerge/automerge@3.3.0-fragments.2': + resolution: {integrity: sha512-F5KL+YXD0GMFH/as0/kJ3+lotpuJsH1SSrhhKdYSGtR+S9ytBNTw+s3pAjF2Vr0+ZdTMHxsGjqj5SRkJiSNokQ==} + + '@automerge/vanillajs@2.5.0': + resolution: {integrity: sha512-18Z179fBkRYhfJOvzcz5NU+TSJOfOBez1ZQqjNnRlmHB0xwaC9P/MFuuc9ujWtJSiRAh6jzB3HxUZpCIjEfXFw==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.9': + resolution: {integrity: sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} + cpu: [x64] + os: [win32] + + '@chee/patchwork-bundles@0.2.0': + resolution: {integrity: sha512-Hc8YhdYM1s/6ij08wMFFBa0+LUyPyW7EBh8yrIsWtBzurWcsHnCtdF7ObZtWVqslkLjlgLqcvpDHYpWhfg6I0g==} + + '@chee/patchwork-llm@automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9': + resolution: {automergeUrl: automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey, type: custom:automerge} + version: 0.3.0 + peerDependencies: + '@automerge/automerge-repo': ^2.5.6 + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@inkandswitch/patchwork-bootloader@0.0.3': + resolution: {integrity: sha512-FPVsH+ncVw4gPWUvOweotcTdL421Dg+/KcYBOUVyqZ3PxpWoLwLPw1jevZVMuGrIURR2MNkLCQqVPd5tlZ2qsA==} + peerDependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@automerge/automerge-repo-keyhive': 0.0.0-alpha.71 + '@automerge/vanillajs': 2.5.0 + '@keyhive/keyhive': 0.0.0-alpha.40 + + '@inkandswitch/patchwork-filesystem@0.0.3': + resolution: {integrity: sha512-x6yqX1Xyr05ug4YfRyfKJ7jSm/zjhMxU7g/e70nz4QbxfLmcG98LHB9qAkFI2zd+VvKLnTvSpPGu2UvJzXGciw==} + + '@inkandswitch/patchwork-filesystem@0.2.9': + resolution: {integrity: sha512-To2zBNQoAC4BT+PwpZrc0aKZhjLOTd0poJJs/wYEP1wX3r9xr97Og9CdLEvsqsVnEIUXEdZvizH0fbmY0VA14w==} + peerDependencies: + '@automerge/automerge': '*' + '@automerge/automerge-repo': '*' + + '@inkandswitch/patchwork-plugins@0.0.5': + resolution: {integrity: sha512-pQv5M7asDdN8J2moTmLoSnqA8FC/vcOuNJnKveL3MzjEhH0GQdIl6vV3k5jxs83ZpB8stwceE85Zsve/QRnVSA==} + + '@inkandswitch/patchwork-providers@0.3.0': + resolution: {integrity: sha512-CUxWbONfOiz5SCzOpX/7zDTMePWgq7e4778eBJpbjC5Et4lnyL2/UrOZ6YJ4HiCG7r7MbfM86GUi63bqjTeutg==} + peerDependencies: + '@automerge/automerge-repo': '*' + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@keyhive/keyhive@0.0.0-alpha.40': + resolution: {integrity: sha512-FlvtX6ZK2q47OqqJZ/8G+SAevKJxjSdldhwJPlhUT9QTIv0LNmJO17pkEtLJ0VVrmPryP0tKQDIaQtmcE1f7Tg==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/serviceworker@0.0.153': + resolution: {integrity: sha512-/cg6dFEkNchJLyRCGo4Gb8mF200qr3xskM5dCPgbtK0OzXxcFcXa6BEBdyG7JksRsTrvCR+V6aFPncoOYAwYhQ==} + + '@vitest/expect@3.2.7': + resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} + + '@vitest/mocker@3.2.7': + resolution: {integrity: sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.7': + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} + + '@vitest/runner@3.2.7': + resolution: {integrity: sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==} + + '@vitest/snapshot@3.2.7': + resolution: {integrity: sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==} + + '@vitest/spy@3.2.7': + resolution: {integrity: sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==} + + '@vitest/utils@3.2.7': + resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-plugin-jsx-dom-expressions@0.40.10: + resolution: {integrity: sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA==} + peerDependencies: + '@babel/core': ^7.20.12 + + babel-preset-solid@1.9.15: + resolution: {integrity: sha512-GBmg1OiPb+OwcH51XbDAKPtvrPfQW7rCJTJxcp8+yhtWwN+kqnbEJk2SgVybd+uhTxTKAvjaFyiQSr/eUZBwzg==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.15 + peerDependenciesMeta: + solid-js: + optional: true + + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + baseline-browser-mapping@2.11.25: + resolution: {integrity: sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.29.0: + resolution: {integrity: sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs58@5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + bs58check@3.0.1: + resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} + + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + cbor-extract@2.2.2: + resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} + hasBin: true + + cbor-x@1.6.6: + resolution: {integrity: sha512-8QiD9PGOxyQHo7s2pzwTBH6lTjqekxPdl9Aq6fXvZgCuCJHOht1puDEA/fTr6mciB76c+M+Gi0qT2i1a4pm4Wg==} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.433: + resolution: {integrity: sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + happy-dom@15.11.7: + resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==} + engines: {node: '>=18.0.0'} + + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + node-releases@2.0.56: + resolution: {integrity: sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==} + engines: {node: '>=18'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + solid-js@1.9.15: + resolution: {integrity: sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg==} + + solid-refresh@0.6.3: + resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} + peerDependencies: + solid-js: ^1.3 + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinyargs@0.1.4: + resolution: {integrity: sha512-5OpdhMRRE70j0zT7mrBpx12FJI+y0EGx8zMe8Vl2/aiwGgpW3X70OVHlsHmHeeG0ncnnWdq1o+k/S5d9ONgzGA==} + engines: {node: '>=14'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.6: + resolution: {integrity: sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==} + engines: {node: '>=14.0.0'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uuid@14.0.2: + resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite-plugin-solid@2.11.14: + resolution: {integrity: sha512-7ZVBt8rpoyqmlwin2kRIUveaHoF6/kulY7gsnD+qFh4nS29V4OPAnw+ojoAspXIjObiL9o1xh9a/nTuYHm02Rw==} + peerDependencies: + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0 + solid-js: ^1.7.2 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + '@testing-library/jest-dom': + optional: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@3.2.7: + resolution: {integrity: sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.7 + '@vitest/ui': 3.2.7 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.33.2: + resolution: {integrity: sha512-8tC7yXgeCvpT8gKEeEje6ikJJG1wpnoLiXe+HfECW8m10ubMd3QxKOwWA8KxPJVBcLwfdRoMKYxIBGYKmo37/A==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@automerge/automerge-repo-keyhive@0.0.0-alpha.71': + dependencies: + '@automerge/automerge-repo': 2.5.0-alpha.2 + '@automerge/automerge-repo-network-websocket': 2.5.0-alpha.2 + '@keyhive/keyhive': 0.0.0-alpha.40 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-broadcastchannel@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo-network-messagechannel@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + debug: 4.4.3 + eventemitter3: 5.0.4 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo-network-websocket@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + isomorphic-ws: 5.0.0(ws@8.21.3) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-network-websocket@2.5.0-alpha.2': + dependencies: + '@automerge/automerge-repo': 2.5.0-alpha.2 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + isomorphic-ws: 5.0.0(ws@8.21.3) + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-storage-indexeddb@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.5.0': + dependencies: + '@automerge/automerge': 3.2.1 + bs58check: 3.0.1 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.5.0-alpha.2': + dependencies: + '@automerge/automerge': 3.2.1 + bs58check: 3.0.1 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.33.2 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.6.0-subduction.40': + dependencies: + '@automerge/automerge': 3.3.0-fragments.2 + '@automerge/automerge-subduction': 0.16.0 + bs58check: 4.0.0 + cbor-x: 1.6.6 + debug: 4.4.3 + eventemitter3: 5.0.4 + fast-sha256: 1.3.0 + isomorphic-ws: 5.0.0(ws@8.21.3) + uuid: 14.0.2 + ws: 8.21.3 + xstate: 5.33.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-subduction@0.16.0': {} + + '@automerge/automerge@3.2.1': {} + + '@automerge/automerge@3.3.0-fragments.2': {} + + '@automerge/vanillajs@2.5.0': + dependencies: + '@automerge/automerge-repo': 2.5.0 + '@automerge/automerge-repo-network-broadcastchannel': 2.5.0 + '@automerge/automerge-repo-network-messagechannel': 2.5.0 + '@automerge/automerge-repo-network-websocket': 2.5.0 + '@automerge/automerge-repo-storage-indexeddb': 2.5.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.29.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.9': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.9 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.2': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.2': + optional: true + + '@chee/patchwork-bundles@0.2.0(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@inkandswitch/patchwork-bootloader': 0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40) + '@inkandswitch/patchwork-filesystem': 0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1) + es-module-lexer: 2.3.2 + resolve.exports: 2.0.3 + unplugin: 2.3.11 + transitivePeerDependencies: + - '@automerge/automerge' + - '@automerge/automerge-repo-keyhive' + - '@automerge/vanillajs' + - '@keyhive/keyhive' + - bufferutil + - supports-color + - utf-8-validate + + '@chee/patchwork-llm@automerge:m6dt2f4hYdhyvmFDvXYbYDdNQey@kHSoEYZn3QrxfLpLC7Lbm2j979WAsxGgNMH8wZd6jbasFTkx9(@automerge/automerge-repo@2.6.0-subduction.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@inkandswitch/patchwork-providers': 0.3.0(@automerge/automerge-repo@2.6.0-subduction.40) + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@inkandswitch/patchwork-bootloader@0.0.3(@automerge/automerge-repo-keyhive@0.0.0-alpha.71)(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)(@automerge/vanillajs@2.5.0)(@keyhive/keyhive@0.0.0-alpha.40)': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.6.0-subduction.40 + '@automerge/automerge-repo-keyhive': 0.0.0-alpha.71 + '@automerge/vanillajs': 2.5.0 + '@keyhive/keyhive': 0.0.0-alpha.40 + '@types/debug': 4.1.13 + debug: 4.4.3 + resolve.exports: 2.0.3 + service-worker-types: '@types/serviceworker@0.0.153' + tinyargs: 0.1.4 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.0.3': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-filesystem@0.2.9(@automerge/automerge-repo@2.6.0-subduction.40)(@automerge/automerge@3.2.1)': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.6.0-subduction.40 + debug: 4.4.3 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-plugins@0.0.5': + dependencies: + '@automerge/automerge': 3.2.1 + '@automerge/automerge-repo': 2.5.0 + '@inkandswitch/patchwork-filesystem': 0.0.3 + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + debug: 4.4.3 + eventemitter3: 5.0.4 + resolve.exports: 2.0.3 + transitivePeerDependencies: + - supports-color + + '@inkandswitch/patchwork-providers@0.3.0(@automerge/automerge-repo@2.6.0-subduction.40)': + dependencies: + '@automerge/automerge-repo': 2.6.0-subduction.40 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@keyhive/keyhive@0.0.0-alpha.40': {} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@noble/hashes@1.8.0': {} + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.9 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/ms@2.1.0': {} + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/serviceworker@0.0.153': {} + + '@vitest/expect@3.2.7': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.7(vite@5.4.21(@types/node@20.19.43))': + dependencies: + '@vitest/spy': 3.2.7 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + '@vitest/pretty-format@3.2.7': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.7': + dependencies: + '@vitest/utils': 3.2.7 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.7': + dependencies: + tinyspy: 4.0.6 + + '@vitest/utils@3.2.7': + dependencies: + '@vitest/pretty-format': 3.2.7 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn@8.18.0: {} + + assertion-error@2.0.1: {} + + babel-plugin-jsx-dom-expressions@0.40.10(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + html-entities: 2.3.3 + parse5: 7.3.0 + + babel-preset-solid@1.9.15(@babel/core@7.29.7)(solid-js@1.9.15): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jsx-dom-expressions: 0.40.10(@babel/core@7.29.7) + optionalDependencies: + solid-js: 1.9.15 + + base-x@4.0.1: {} + + base-x@5.0.1: {} + + baseline-browser-mapping@2.11.25: {} + + browserslist@4.29.0: + dependencies: + baseline-browser-mapping: 2.11.25 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.433 + node-releases: 2.0.56 + update-browserslist-db: 1.3.3(browserslist@4.29.0) + + bs58@5.0.0: + dependencies: + base-x: 4.0.1 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + bs58check@3.0.1: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 5.0.0 + + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 6.0.0 + + cac@6.7.14: {} + + caniuse-lite@1.0.30001810: {} + + cbor-extract@2.2.2: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm': 2.2.2 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 + '@cbor-extract/cbor-extract-linux-x64': 2.2.2 + '@cbor-extract/cbor-extract-win32-x64': 2.2.2 + optional: true + + cbor-x@1.6.6: + optionalDependencies: + cbor-extract: 2.2.2 + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + detect-libc@2.1.2: + optional: true + + electron-to-chromium@1.5.433: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + es-module-lexer@1.7.0: {} + + es-module-lexer@2.3.2: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + eventemitter3@5.0.4: {} + + expect-type@1.4.0: {} + + fast-sha256@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + happy-dom@15.11.7: + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + + html-entities@2.3.3: {} + + is-what@4.1.16: {} + + isomorphic-ws@5.0.0(ws@8.21.3): + dependencies: + ws: 8.21.3 + + js-tokens@4.0.0: {} + + js-tokens@9.0.1: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + loupe@3.2.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + + ms@2.1.3: {} + + nanoid@3.3.19: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.56: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve.exports@2.0.3: {} + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + semver@6.3.1: {} + + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + + siginfo@2.0.0: {} + + solid-js@1.9.15: + dependencies: + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + + solid-refresh@0.6.3(solid-js@1.9.15): + dependencies: + '@babel/generator': 7.29.8 + '@babel/helper-module-imports': 7.29.7 + '@babel/types': 7.29.8 + solid-js: 1.9.15 + transitivePeerDependencies: + - supports-color + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinyargs@0.1.4: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.6: {} + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.18.0 + picomatch: 4.0.7 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.3.3(browserslist@4.29.0): + dependencies: + browserslist: 4.29.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + uuid@14.0.2: {} + + uuid@9.0.1: {} + + vite-node@3.2.4(@types/node@20.19.43): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 5.4.21(@types/node@20.19.43) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite-plugin-solid@2.11.14(solid-js@1.9.15)(vite@5.4.21(@types/node@20.19.43)): + dependencies: + '@babel/core': 7.29.7 + '@types/babel__core': 7.20.5 + babel-preset-solid: 1.9.15(@babel/core@7.29.7)(solid-js@1.9.15) + merge-anything: 5.1.7 + solid-js: 1.9.15 + solid-refresh: 0.6.3(solid-js@1.9.15) + vite: 5.4.21(@types/node@20.19.43) + vitefu: 1.1.3(vite@5.4.21(@types/node@20.19.43)) + transitivePeerDependencies: + - supports-color + + vite@5.4.21(@types/node@20.19.43): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.28 + rollup: 4.63.4 + optionalDependencies: + '@types/node': 20.19.43 + fsevents: 2.3.3 + + vitefu@1.1.3(vite@5.4.21(@types/node@20.19.43)): + optionalDependencies: + vite: 5.4.21(@types/node@20.19.43) + + vitest@3.2.7(@types/debug@4.1.13)(@types/node@20.19.43)(happy-dom@15.11.7): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.7 + '@vitest/mocker': 3.2.7(vite@5.4.21(@types/node@20.19.43)) + '@vitest/pretty-format': 3.2.7 + '@vitest/runner': 3.2.7 + '@vitest/snapshot': 3.2.7 + '@vitest/spy': 3.2.7 + '@vitest/utils': 3.2.7 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.4.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 5.4.21(@types/node@20.19.43) + vite-node: 3.2.4(@types/node@20.19.43) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 20.19.43 + happy-dom: 15.11.7 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + webidl-conversions@7.0.0: {} + + webpack-virtual-modules@0.6.2: {} + + whatwg-mimetype@3.0.0: {} + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.3: {} + + xstate@5.33.2: {} + + yallist@3.1.1: {} diff --git a/llm-host/pnpm-workspace.yaml b/llm-host/pnpm-workspace.yaml new file mode 100644 index 00000000..b6a60e31 --- /dev/null +++ b/llm-host/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +# pnpm 11 reads its settings from this file. There is no root workspace, so +# every package carries the settings it needs rather than inheriting them. +minimumReleaseAge: 0 +verifyDepsBeforeRun: false +allowBuilds: + "@swc/core": true + cbor-extract: true + core-js: true + esbuild: true +configDependencies: + pnpm-plugin-patchwork: "0.4.1+sha512-IzQJkoeQaSaYSa1bRko3KUJ+6nBmYg8Qxuyy0llnhPfAGSDP/+gM/Z7R0HGLtTpCE3K57CXS9DnAItqb/pNIZQ==" diff --git a/llm-host/src/LlmConfigTray.tsx b/llm-host/src/LlmConfigTray.tsx new file mode 100644 index 00000000..de861545 --- /dev/null +++ b/llm-host/src/LlmConfigTray.tsx @@ -0,0 +1,216 @@ +import { createSignal, onCleanup, Show } from "solid-js"; +import { Portal, render } from "solid-js/web"; +// @ts-ignore — plain-JS library, ships no type declarations +import { + dom as pickerDom, + ensureConfig, + readScopedConfig, + readConfig, + writeScopeOverride, + writeConfig, +} from "@chee/patchwork-llm"; + +/** + * The LLM config tray tool — a `patchwork:component` (element) => cleanup. + * + * Runs in the HOST realm (it's a system-tray icon). It owns the picker UI over + * the LLM settings doc. Chat (and anything else) opens it either by clicking the + * tray icon or by dispatching a `patchwork:open-tool` + * {detail:{component:"llm-config-tray", scope, toolPrompt, toolTools}} event + * (relayed across the isolation boundary by the isolation open-tool bridge). + * + * The picker is driven through its injectable `source` {read, write} so this tool + * is the sole owner of settings-doc writes; per-tool/per-doc scope overrides are + * written via writeScopeOverride, the global default via writeConfig. + */ + +/** @typedef {{toolId?: string, docId?: string, toolName?: string, docName?: string}} Scope */ + +export function LlmConfigTray(element: HTMLElement) { + const style = document.createElement("style"); + style.textContent = ` + .llm-config-tray { position: relative; display: inline-flex; align-items: center; } + .llm-config-tray-button { + display: inline-flex; align-items: center; gap: var(--studio-space-2xs, 0.25rem); + height: 1.75rem; padding: 0 var(--studio-space-xs, 0.375rem); + border: 1px solid var(--studio-chrome-offset-20, var(--studio-fill-offset-20, #d4d4d4)); + border-radius: var(--studio-radius-sm, 4px); + background: var(--studio-chrome, var(--studio-fill, white)); + color: var(--studio-chrome-line, var(--studio-line, black)); + font: 500 0.75rem/1 var(--studio-family-sans, system-ui, sans-serif); cursor: pointer; + } + .llm-config-tray-button:hover { + background: var(--studio-chrome-offset-10, var(--studio-fill-offset-10, #f2f2f2)); + } + .llm-config-tray-popover { + position: fixed; z-index: 2147483000; + max-width: min(92vw, 520px); max-height: 80vh; overflow: auto; + background: var(--studio-fill, white); color: var(--studio-line, black); + border: 1px solid var(--studio-fill-offset-20, #d4d4d4); + border-radius: var(--studio-radius-md, 8px); + box-shadow: var(--studio-shadow-lg, 0 8px 32px rgba(0,0,0,0.24)); + padding: var(--studio-space-sm, 0.5rem); + } + `; + element.append(style); + + // Current scope the picker edits (set when opened via event; icon click uses + // the global default scope). + const [open, setOpen] = createSignal(false); + const [scope, setScope] = createSignal({}); + const [payload, setPayload] = createSignal({}); + const [pos, setPos] = createSignal<{ left: number; top: number } | null>( + null + ); + + let buttonEl!: HTMLButtonElement; + let popoverEl: HTMLDivElement | undefined; + let pickerHost: HTMLDivElement | undefined; + let currentPicker: any = null; + + // Build the picker's source from the current scope: scoped reads/writes when a + // toolId is present, else the global default config. + function sourceFor(sc: Scope) { + if (sc && sc.toolId) { + return { + read: () => { + try { + return readScopedConfig(sc); + } catch { + return readConfig(); + } + }, + write: (next: any) => writeScopeOverride(sc, next), + }; + } + return { + read: () => readConfig(), + write: (next: any) => writeConfig(next), + }; + } + + async function mountPicker() { + const sc = scope(); + const p = payload() || {}; + await ensureConfig(sc && sc.toolId ? sc : undefined, element); + if (!pickerHost) return; + pickerHost.replaceChildren(); + currentPicker = pickerDom({ + source: sourceFor(sc), + scope: sc && sc.toolId ? sc : undefined, + toolName: sc?.toolName || p.toolName, + toolPrompt: p.toolPrompt, + toolTools: p.toolTools, + }); + pickerHost.append(currentPicker); + } + + function positionPopover() { + const r = buttonEl.getBoundingClientRect(); + // Prefer opening above the tray (tray sits at the bottom), aligned right. + setPos({ left: Math.max(8, r.right - 520), top: Math.max(8, r.top - 8) }); + } + + function openPicker(sc: Scope, p: any) { + setScope(sc || {}); + setPayload(p || {}); + positionPopover(); + setOpen(true); + queueMicrotask(mountPicker); + } + + function closePicker() { + try { + currentPicker?.destroy?.(); + } catch {} + currentPicker = null; + setOpen(false); + } + + // Open via the `patchwork:open-tool` event (from chat / other tools, incl. + // relayed across isolation). Match our component id. + const onOpenTool = (e: Event) => { + const detail = (e as CustomEvent).detail; + if (!detail || detail.component !== "llm-config-tray") return; + openPicker(detail.scope || {}, detail); + }; + document.addEventListener("patchwork:open-tool", onOpenTool as EventListener); + + // Close on outside click. Use composedPath() (captured at event time, before + // the picker re-renders its own DOM on a selection) rather than + // popoverEl.contains(target): the picker rebuilds its option list when config + // changes, so the clicked node is often already detached by the time the click + // bubbles to document — contains() would then wrongly report "outside" and + // close the popover on every model click. + const onDocClick = (e: MouseEvent) => { + if (!open()) return; + const path = e.composedPath(); + if ( + (popoverEl && path.includes(popoverEl)) || + (buttonEl && path.includes(buttonEl)) + ) { + return; // click was inside the popover or on the tray button + } + closePicker(); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape" && open()) closePicker(); + }; + document.addEventListener("click", onDocClick); + document.addEventListener("keydown", onKey); + + const dispose = render( + () => ( +
+ + + +
event.stopPropagation()} + > +
+
+ + +
+ ), + element + ); + + onCleanup(() => { + try { + currentPicker?.destroy?.(); + } catch {} + }); + + return () => { + dispose(); + style.remove(); + document.removeEventListener( + "patchwork:open-tool", + onOpenTool as EventListener + ); + document.removeEventListener("click", onDocClick); + document.removeEventListener("keydown", onKey); + }; +} diff --git a/llm-host/src/client.js b/llm-host/src/client.js new file mode 100644 index 00000000..a5fd83ce --- /dev/null +++ b/llm-host/src/client.js @@ -0,0 +1,106 @@ +/** + * The LLM consume-half — the mirror of worker-spec.js. + * + * worker-spec.js (the SERVE half) turns a consumer's request frames into worker + * traffic; this file (the CONSUME half) turns a tool's `generate(...)` call into + * those request frames and reads the event stream back. Together they are the LLM + * *protocol* layer, sitting above the generic transport (@grjte/patchwork-worker) + * and below any tool that wants to generate. + * + * How a tool gets it: NOT by import. index.ts registers `makeLLMClient` as the + * `patchwork:worker-client` plugin with id "llm", paired with the `patchwork:worker` + * plugin of the same id. A tool calls the transport's + * `connectWorkerClient("llm", …)`, which resolves this factory from the plugin + * registry, opens a session for "llm", and returns `makeLLMClient(session)`. So + * both halves of the protocol ship in this one package and cannot drift, and the + * tool has no build-time dependency on it. + * + * ⚠ Imports NOTHING — and must never import @chee/patchwork-llm or + * ./worker-spec.js. This chunk is what a sandboxed tool loads (via the plugin's + * `load()`); the library and the serve half stay host-side. + * + * Tool calls: the serve half parses `` text host-side for providers + * without native function calling, so `result.toolCalls` is populated (or null) + * for every provider and a consumer never parses model text itself. + * + * Frame vocabulary (must stay in step with worker-spec.js): + * request: {id, op:"generate", scope, system, tools, messages|text, sessionKey} + * events: {id, type:"token"|"stats"|"prediction"|"status"|"model"| + * "result"|"error", ...} result/error terminal. + */ + +/** + * @typedef {Object} GenOpts + * @property {any} [scope] {toolId, docId} — the host resolves config for this scope + * @property {string|Record} [system] string | {default, [provider]} + * @property {any[]} [tools] tool descriptors {name, description, defaultOff?, parameters?} + * @property {string} [sessionKey] + * @property {HTMLElement} [element] a node in the mounted , for discovery + * @property {AbortSignal} [signal] + * @property {(delta:string, full:string)=>void} [onToken] + * @property {(message:string)=>void} [onStatus] + * @property {(label:string)=>void} [onModel] + * + * @typedef {Object} GenResult + * @property {string} text + * @property {any[]|null} toolCalls + * @property {string} [toolMode] + * + * @typedef {{generate: (messages: any[]|string, opts?: GenOpts) => Promise}} LLMClient + */ + +/** + * The `patchwork:worker-client` factory for kind "llm": bind the LLM protocol to + * an already-open transport session and return the client API. + * + * @param {{request: (frame:any, opts:any)=>{promise:Promise}}} session + * @returns {LLMClient} + */ +export function makeLLMClient(session) { + /** + * Generate a completion. Streams tokens via opts.onToken; resolves with the + * final text + any structured tool calls. Tool EXECUTION stays in the tool — + * the worker only receives tool descriptors and returns calls to run. + * @param {any[]|string} messages + * @param {GenOpts} [opts] + * @returns {Promise} + */ + function generate(messages, opts = {}) { + const payload = typeof messages === "string" ? {text: messages} : {messages} + + const {promise} = session.request( + { + op: "generate", + sessionKey: opts.sessionKey, + scope: opts.scope, + system: opts.system, + tools: opts.tools, + ...payload, + }, + { + element: opts.element, + signal: opts.signal, + terminal: { + result: (/** @type {any} */ f) => ({ + text: f.text, + toolCalls: f.toolCalls || null, + toolMode: f.toolMode, + }), + error: (/** @type {any} */ f) => { + throw new Error(f.message) + }, + }, + // prediction / stats frames are ignored by the caller unless it asks. + onFrame: (/** @type {any} */ f) => { + if (f.type === "token") opts.onToken?.(f.delta, f.text) + else if (f.type === "status") opts.onStatus?.(f.message) + else if (f.type === "model") opts.onModel?.(f.label) + }, + } + ) + + return /** @type {Promise} */ (promise) + } + + return {generate} +} diff --git a/llm-host/src/client.test.js b/llm-host/src/client.test.js new file mode 100644 index 00000000..45fe38a6 --- /dev/null +++ b/llm-host/src/client.test.js @@ -0,0 +1,112 @@ +import {describe, it, expect, vi} from "vitest" +import {makeLLMClient} from "./client.js" + +/** + * A fake transport session: records the request frame + opts and lets the test + * drive the terminal/onFrame callbacks the way session.js would. + */ +function fakeSession() { + /** @type {any} */ + const seen = {frame: null, opts: null} + let settle = /** @type {any} */ (null) + const session = { + request(frame, opts) { + seen.frame = frame + seen.opts = opts + const promise = new Promise((resolve, reject) => { + settle = {resolve, reject} + }) + return {promise, abort: vi.fn()} + }, + } + /** Deliver a terminal frame through opts.terminal, like session.js does. */ + const terminal = (f) => { + try { + settle.resolve(seen.opts.terminal[f.type](f)) + } catch (e) { + settle.reject(e) + } + } + const frame = (f) => seen.opts.onFrame(f) + return {session, seen, terminal, frame} +} + +describe("makeLLMClient", () => { + it("returns the client API: {generate} only", () => { + const {session} = fakeSession() + const client = makeLLMClient(session) + expect(Object.keys(client)).toEqual(["generate"]) + expect(typeof client.generate).toBe("function") + }) + + it("sends a generate frame with messages, scope, system, tools and sessionKey", async () => { + const {session, seen, terminal} = fakeSession() + const client = makeLLMClient(session) + const el = {} + const signal = new AbortController().signal + const p = client.generate([{role: "user", content: "hi"}], { + sessionKey: "doc-1", + scope: {toolId: "chat"}, + system: {default: "be brief"}, + tools: [{name: "x"}], + element: /** @type {any} */ (el), + signal, + }) + expect(seen.frame).toEqual({ + op: "generate", + sessionKey: "doc-1", + scope: {toolId: "chat"}, + system: {default: "be brief"}, + tools: [{name: "x"}], + messages: [{role: "user", content: "hi"}], + }) + expect(seen.opts.element).toBe(el) + expect(seen.opts.signal).toBe(signal) + terminal({type: "result", text: "hello", toolCalls: null}) + await expect(p).resolves.toEqual({text: "hello", toolCalls: null, toolMode: undefined}) + }) + + it("sends a string prompt as text", async () => { + const {session, seen, terminal} = fakeSession() + const p = makeLLMClient(session).generate("continue this") + expect(seen.frame.text).toBe("continue this") + expect(seen.frame.messages).toBeUndefined() + terminal({type: "result", text: "…"}) + await p + }) + + it("passes structured toolCalls and toolMode through from the result frame", async () => { + const {session, terminal} = fakeSession() + const p = makeLLMClient(session).generate("x") + terminal({type: "result", text: "t", toolCalls: [{name: "x", args: {}}], toolMode: "template"}) + await expect(p).resolves.toEqual({ + text: "t", + toolCalls: [{name: "x", args: {}}], + toolMode: "template", + }) + }) + + it("rejects with the error frame's message", async () => { + const {session, terminal} = fakeSession() + const p = makeLLMClient(session).generate("x") + terminal({type: "error", message: "boom"}) + await expect(p).rejects.toThrow("boom") + }) + + it("routes token / status / model frames to the callbacks", async () => { + const {session, frame, terminal} = fakeSession() + const onToken = vi.fn() + const onStatus = vi.fn() + const onModel = vi.fn() + const p = makeLLMClient(session).generate("x", {onToken, onStatus, onModel}) + frame({type: "token", delta: "h", text: "h"}) + frame({type: "status", message: "loading"}) + frame({type: "model", label: "Local 1B"}) + frame({type: "stats", tokPerSec: 1}) // ignored + expect(onToken).toHaveBeenCalledWith("h", "h") + expect(onStatus).toHaveBeenCalledWith("loading") + expect(onModel).toHaveBeenCalledWith("Local 1B") + terminal({type: "result", text: "h"}) + await p + }) +}) diff --git a/llm-host/src/index.ts b/llm-host/src/index.ts new file mode 100644 index 00000000..700b4f0c --- /dev/null +++ b/llm-host/src/index.ts @@ -0,0 +1,81 @@ +import type { Plugin } from "@inkandswitch/patchwork-plugins"; + +/** + * @grjte/llm-host — the host-realm LLM package. + * + * `@chee/patchwork-llm` is a plain library (npm-bound, no Patchwork coupling). + * This package is the Patchwork side of it: it registers what the platform needs + * to know about, and owns everything that must run in the host realm because it + * touches the settings doc and the API key. + * + * Three plugins: + * + * `llm-config-tray` (patchwork:component) + * The system-tray icon that opens the model/config picker over the LLM + * settings doc. Consumer tools never read config — they open this tool via a + * `patchwork:open-tool` event (relayed across the isolation boundary) and + * otherwise just name a scope when they generate. + * + * `llm` (patchwork:worker) + * The LLM worker itself. The generic worker provider + * (`@grjte/patchwork-worker`) resolves it by this id when a tool calls + * `connectWorker("llm", …)`, in or out of isolation. Registering it + * declaratively is what keeps the provider kind-agnostic: it no longer + * imports the LLM, so nothing service-specific loads until someone actually + * connects. + * + * `llm` (patchwork:worker-client) + * The CONSUME half of the same protocol, paired with the worker by id. Its + * `load()` resolves to `makeLLMClient`, a factory `(session) => {generate}`. + * A tool calls `connectWorkerClient("llm", …)` from @grjte/patchwork-worker, + * which resolves this factory from the registry and binds it to a session — + * so the tool never imports this package. Both halves ship here and cannot + * drift. Under isolation the registry is mirrored into the iframe and the + * iframe loads this package's entry + the client chunk; nothing else of + * llm-host (and none of @chee/patchwork-llm) enters the sandbox. + * + * All three are behind `load()`, so importing this module registers descriptors + * only — the library (worker, config, picker, transformers.js glue) is imported + * lazily on first use rather than eagerly at frame mount. Descriptor fields must + * stay plain data: the isolation bridge structured-clones them. + */ +export const plugins: Plugin[] = [ + { + type: "patchwork:component", + id: "llm-config-tray", + name: "LLM Model & Config", + icon: "Bot", + tags: ["system-tray"], + async load() { + const { LlmConfigTray } = await import("./LlmConfigTray.js"); + return LlmConfigTray; + }, + }, + { + type: "patchwork:worker", + id: "llm", + name: "LLM", + async load() { + // Resolves to a WorkerSpec that @grjte/patchwork-worker serves. The library + // supplies the compute worker (createWorker) and the config/tool functions; + // the spec below adds the LLM op vocabulary and the settings-doc resolution + // that must run host-side (the key/settings doc never cross into a sandbox). + // @ts-ignore — plain-JS library, ships no bundled types + const lib = await import("@chee/patchwork-llm"); + // @ts-ignore — plain-JS sibling, JSDoc-typed + const { makeLLMWorkerSpec } = await import("./worker-spec.js"); + return makeLLMWorkerSpec(lib); + }, + }, + { + type: "patchwork:worker-client", + id: "llm", + name: "LLM client", + async load() { + // The consume half: pure, imports nothing, safe to load into a sandbox. + // @ts-ignore — plain-JS sibling, JSDoc-typed + const { makeLLMClient } = await import("./client.js"); + return makeLLMClient; + }, + }, +]; diff --git a/llm-host/src/worker-spec.js b/llm-host/src/worker-spec.js new file mode 100644 index 00000000..ca5d9d10 --- /dev/null +++ b/llm-host/src/worker-spec.js @@ -0,0 +1,377 @@ +/** + * The LLM WorkerSpec — the host-realm glue that turns @chee/patchwork-llm into a + * `patchwork:worker` that @grjte/patchwork-worker can serve. + * + * The generic transport (@grjte/patchwork-worker/serve.js) owns the stream pair, + * the per-connection worker lifetime, id demux, and abort plumbing. This file + * owns everything LLM-specific: resolving config/secrets from the settings doc, + * building tool schemas, the op vocabulary (generate / predict / the analytical + * ops / preload), and the model label. None of that can live in the sandbox — + * the settings doc is denylisted and the API key must never cross — which is why + * it lives HERE, in the host-realm package, rather than in the library or in the + * transport. + * + * `makeLLMWorkerSpec(lib)` takes the loaded @chee/patchwork-llm module (so this + * file has no static dependency on it — llm-host loads the library from its + * automerge doc at runtime) and returns the spec. + * + * Request preparation (prompt resolution, CallConfig, the native-vs-templated + * tools decision, the builtin detour, the input shape, the worker payload) is + * the library's `prepareGenerate` / `buildGeneratePayload` — the same code its + * own same-realm `generate()` runs — so the two paths cannot diverge. What stays + * here is host policy: which overrides a consumer may set, provider-conditional + * system maps, `toolToggles`, and parsing `` text into `toolCalls` so + * consumers (src/client.js, the consume half) never parse model output. + */ + +/** @param {...any} args */ +function clog(...args) { + try { + console.log("[llm-host]", ...args) + } catch {} +} + +/** + * Resolve a (possibly provider-conditional) system prompt. `system` may be a + * plain string (used as-is) or a map like `{default, local, openrouter, …}` — + * the entry matching the provider wins, falling back to `default`. + * @param {any} system + * @param {string} provider + */ +function resolveSystemForProvider(system, provider) { + if (system == null) return undefined + if (typeof system === "string") return system + if (typeof system === "object") return system[provider] ?? system.default ?? undefined + return undefined +} + +/** + * Filter tool descriptors by the config's `toolToggles`. A tool is included + * unless turned off (`toggles[name] === false`); a `defaultOff` tool is opt-in + * (included only if `toggles[name] === true`). Matching is by descriptor `name`. + * @param {any[]} tools + * @param {Record} toggles + */ +function filterToolsByToggles(tools, toggles) { + if (!Array.isArray(tools)) return tools + const t = toggles || {} + return tools.filter((tool) => { + if (tool && tool.defaultOff) return t[tool.name] === true + return t[tool.name] !== false + }) +} + +/** + * Build the LLM WorkerSpec. + * @param {any} lib the loaded @chee/patchwork-llm module + */ +export function makeLLMWorkerSpec(lib) { + const { + createWorker, + ensureConfig, + ensureSettingsDoc, + settingsDocHandle, + callConfig, + applyPrompts, + effectiveSystem, + describeConfig, + fetchOpenRouterModels, + builtinGenerate, + resolveCfgPrompts, + prepareGenerate, + buildGeneratePayload, + parseToolCalls, + } = lib + + // OpenRouter catalogue cache (host-side): turning a model id into a human name + // needs the catalogue. Fetch once and reuse; failure just yields the raw id. + /** @type {any[]|null} */ + let openrouterModelsCache = null + async function openrouterModels() { + if (openrouterModelsCache) return openrouterModelsCache + try { + openrouterModelsCache = await fetchOpenRouterModels() + } catch { + openrouterModelsCache = [] + } + return openrouterModelsCache + } + + /** + * Human-readable label for the resolved config. Consults the OpenRouter + * catalogue for that provider. Never includes secrets. + * @param {any} cfg + */ + async function modelLabel(cfg) { + try { + const openrouter = cfg.provider === "openrouter" ? (await openrouterModels()) || [] : [] + return describeConfig(cfg, {openrouterModels: openrouter}) + } catch { + return "the configured model" + } + } + + /** + * Resolve the unresolved config for a request frame — the DOM/repo-bound + * preamble. Config is resolved HERE from the frame's `scope`, never from a + * consumer-supplied `frame.config` (which would let a tool pick the provider + * AND supply its own key, defeating host-side resolution). + * + * @param {any} frame + * @param {Promise|null} settingsWarm the connection's in-flight + * settings-doc resolution (spec.open), awaited before reading config. + */ + async function resolveCfg0(frame, settingsWarm) { + if (settingsWarm) await settingsWarm + let cfg0 = await ensureConfig(frame.scope) + // Self-heal a cold config: `ensureConfig` resolves elementless and on its + // retryable paths silently yields DEFAULTS (local model for an OpenRouter + // account, or OpenRouter with an empty apiKey → 401). The warm normally + // settles this; this retry covers a caller whose warm timed out, and + // `ensureSettingsDoc` deliberately doesn't cache its null results. + if (!settingsDocHandle()) { + await ensureSettingsDoc() + cfg0 = await ensureConfig(frame.scope) + } + return cfg0 + } + + /** + * Only the sampling knobs a consumer legitimately owns are forwarded to + * callConfig. The frame is NOT passed wholesale: callConfig also honours + * provider / apiKey / model / url overrides, so handing it the raw frame would + * let a consumer redirect generation at its own endpoint with its own key. + * @param {any} frame + */ + function samplingOverrides(frame) { + return { + temperature: frame.temperature, + topP: frame.topP, + topk: frame.topk, + maxNewTokens: frame.maxNewTokens, + } + } + + /** + * Prepare a `generate` frame with the library's own request preparation, + * after applying host policy: the provider-conditional system map and the + * user's toolToggles. Returns what the library's generate() would have + * computed, plus `hasTools` (whether any tool survived the toggles). + * + * @param {any} frame + * @param {Promise|null} settingsWarm + */ + async function prepareFrame(frame, settingsWarm) { + const cfg0 = await resolveCfg0(frame, settingsWarm) + // The provider is fixed by the settings doc: sampling overrides never carry + // one and prompt resolution doesn't change it, so cfg0.provider is what + // callConfig will report. + const tools = filterToolsByToggles(frame.tools, cfg0.toolToggles || {}) + const prepared = await prepareGenerate(cfg0, { + messages: frame.text != null ? frame.text : frame.messages, + system: resolveSystemForProvider(frame.system, cfg0.provider), + tools, + // A continuation is a raw string prompt with no chat messages. + continuation: !!frame.continuation && typeof frame.messages === "undefined", + overrides: samplingOverrides(frame), + }) + return {...prepared, hasTools: Array.isArray(tools) && tools.length > 0} + } + + /** + * Config for the non-generate ops (predict, the analytical ops, preload): + * resolved prompts + the flat CallConfig with sampling overrides only. + * @param {any} frame + * @param {Promise|null} settingsWarm + */ + async function resolveForFrame(frame, settingsWarm) { + const cfg0 = await resolveCfg0(frame, settingsWarm) + const cfg = await resolveCfgPrompts(cfg0) + const config = callConfig(cfg, samplingOverrides(frame)) + return {cfg, config} + } + + /** + * The `toolCalls` a result frame should carry. Native function-calling + * providers return structured calls; for the rest (local / builtin) the model + * writes `` text, which is parsed HERE so every consumer sees the + * same shape and never parses model output itself. Parsing is gated on the + * request actually offering tools — prose that happens to contain a bare + * `{"name": …}` object is not a tool call. `null` when there are none. + * + * @param {string} text + * @param {any[]|null|undefined} toolCalls structured calls from the worker + * @param {boolean} hasTools + * @returns {any[]|null} + */ + function withToolCalls(text, toolCalls, hasTools) { + if (Array.isArray(toolCalls) && toolCalls.length) return toolCalls + if (!hasTools) return null + const parsed = parseToolCalls(text || "") + return parsed.length ? parsed : null + } + + return { + createWorker, + + /** + * Warm the settings doc before any frame is handled. Returned as opaque + * state; the transport bounds it with a timeout and passes it to `handle`, + * where `resolveForFrame` awaits it. `ctx.element` reaches the + * `patchwork:tool-storage` provider through the DOM; without it config + * silently falls back to DEFAULTS (the 401 case). + * @param {{element?: HTMLElement}} ctx + */ + open(ctx) { + return ctx.element ? Promise.resolve(ensureSettingsDoc(ctx.element)).catch(() => null) : null + }, + + /** + * Turn one consumer frame into worker traffic. Returns an abort token + * (`{sessionKey}`) so `op:"abort"` can cancel the matching worker request. + * + * `io` comes from @grjte/patchwork-worker's serveWorkerSpec: + * post(msg, transfer?) send to the worker + * emit(frame) enqueue onto this consumer's readable (id-tagged + * by the transport; we emit worker-shaped frames) + * on(fn) handle worker messages for this workerId; return + * true from fn when the request is complete + * workerId the transport-minted id to tag worker payloads + * state whatever open() resolved (the settings warm) + * + * @param {any} frame + * @param {any} io + */ + async handle(frame, io) { + const {post, emit, on, workerId, state: settingsWarm} = io + const op = frame.op + const sessionKey = frame.sessionKey || workerId + + if (op === "generate") { + const {cfg, config, extraSystem, builtin, input, hasTools} = await prepareFrame( + frame, + settingsWarm + ) + // Tell the consumer which model is answering (host-computed; no secrets). + void modelLabel(cfg).then((label) => emit({type: "model", label})) + + // builtin (Chrome Prompt API) runs in-realm, not in the worker. + if (builtin) { + try { + const full = await builtinGenerate(input, { + temperature: config.temperature, + topK: config.topK, + system: effectiveSystem(cfg, extraSystem), + onToken: (_d, f) => emit({type: "token", delta: "", text: f}), + onStatus: (m) => emit({type: "status", message: m}), + signal: undefined, + }) + emit({type: "result", text: full, toolCalls: withToolCalls(full, null, hasTools)}) + } catch (e) { + const err = /** @type {any} */ (e) + emit({type: "error", message: err?.message || String(e)}) + } + return {sessionKey} + } + + on((msg) => { + switch (msg.type) { + case "token": + case "prediction": + case "stats": + emit(msg) + return false + case "result": + emit({...msg, toolCalls: withToolCalls(msg.text, msg.toolCalls, hasTools)}) + return true + case "error": + clog("generate: worker error", msg.message) + emit(msg) + return true + } + return false + }) + post(buildGeneratePayload(config, input, {id: workerId, sessionKey})) + return {sessionKey} + } + + if (op === "predict") { + const {cfg, config} = await resolveForFrame(frame, settingsWarm) + config.continuation = !!frame.continuation + if (config.provider === "builtin") { + emit({type: "predictions", candidates: []}) + return {sessionKey} + } + const promptedText = applyPrompts(frame.text, cfg, frame.system) + on((msg) => { + if (msg.type === "predictions" || msg.type === "error") { + emit(msg) + return true + } + return false + }) + post({type: "predict", id: workerId, sessionKey, provider: config.provider, text: promptedText, config}) + return {sessionKey} + } + + // Analytical ops: local-only, single terminal result frame. + /** @type {Record} */ + const analytical = { + "score-tokens": ["score-progress", "token-scores"], + "compute-importance": ["importance-scores"], + "compute-attention-weights": ["attention-weights"], + "extract-features": ["features"], + "extract-cut-features": ["cut-features"], + "decode-tokens": ["decoded-tokens"], + "probe-attention": ["probe-attention-result"], + } + if (analytical[op]) { + const {config} = await resolveForFrame(frame, settingsWarm) + const [progressType, terminalType] = analytical[op] + const terminal = terminalType || progressType + on((msg) => { + if (msg.type === terminal) { + emit(msg) + return true + } + if (progressType && terminalType && msg.type === progressType) { + emit(msg) + return false + } + if (msg.type === "error") { + emit(msg) + return true + } + return false + }) + /** @type {any} */ + const payload = {type: op, id: workerId, sessionKey, provider: config.provider, config} + if (frame.text != null) payload.text = frame.text + if (frame.ids != null) payload.ids = frame.ids + post(payload) + return {sessionKey} + } + + if (op === "preload") { + const {config} = await resolveForFrame(frame, settingsWarm) + post({type: "preload", provider: config.provider, config}) + return {sessionKey} + } + + // Unknown op — nothing to do. + return {sessionKey} + }, + + /** + * Cancel one in-flight request. The worker keys `activeGenerations` by + * `sessionKey`, so that is what we post. + * @param {{sessionKey: string}} token + * @param {(m:any)=>void} post + */ + abort(token, post) { + try { + post({type: "abort", sessionKey: token.sessionKey}) + } catch {} + }, + } +} diff --git a/llm-host/src/worker-spec.test.js b/llm-host/src/worker-spec.test.js new file mode 100644 index 00000000..1d6367bb --- /dev/null +++ b/llm-host/src/worker-spec.test.js @@ -0,0 +1,143 @@ +import {describe, it, expect, vi, beforeEach} from "vitest" +import {makeLLMWorkerSpec} from "./worker-spec.js" + +/** + * A fake @chee/patchwork-llm: just enough for the spec's host policy to be + * observable. `prepareGenerate` echoes what it was given so the test can assert + * on the overrides / system / tools the spec chose to forward. + */ +function fakeLib(cfg = {provider: "local", toolToggles: {}}) { + const lib = { + createWorker: () => ({}), + ensureConfig: vi.fn(async () => cfg), + ensureSettingsDoc: vi.fn(async () => ({})), + settingsDocHandle: () => ({}), + callConfig: (c, o) => ({provider: c.provider, ...o}), + applyPrompts: (input) => input, + effectiveSystem: () => "", + describeConfig: () => "Model", + fetchOpenRouterModels: async () => [], + builtinGenerate: vi.fn(async () => "built-in text"), + resolveCfgPrompts: async (c) => c, + prepareGenerate: vi.fn(async (cfg0, opts) => ({ + cfg: cfg0, + config: {provider: cfg0.provider, ...opts.overrides}, + extraSystem: opts.system, + builtin: cfg0.provider === "builtin", + input: opts.messages, + })), + buildGeneratePayload: (config, input, ids) => ({type: "generate", ...ids, provider: config.provider, config, messages: input}), + parseToolCalls: vi.fn((text) => (text.includes("") ? [{name: "x", args: {}}] : [])), + } + return lib +} + +function fakeIO() { + /** @type {any} */ + const io = { + post: vi.fn(), + emit: vi.fn(), + handler: null, + on: (fn) => { + io.handler = fn + }, + workerId: "w1", + state: null, + } + return io +} + +const flush = () => new Promise((r) => setTimeout(r, 0)) + +describe("makeLLMWorkerSpec · generate", () => { + let lib, spec, io + beforeEach(() => { + lib = fakeLib() + spec = makeLLMWorkerSpec(lib) + io = fakeIO() + }) + + it("parses text host-side when the request offered tools", async () => { + await spec.handle({op: "generate", messages: [{role: "user", content: "hi"}], tools: [{name: "x"}]}, io) + io.handler({type: "result", text: "{}", toolCalls: null, toolMode: "template"}) + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + expect(result.toolMode).toBe("template") + expect(lib.parseToolCalls).toHaveBeenCalledOnce() + }) + + it("passes native toolCalls through untouched", async () => { + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + const native = [{name: "x", args: {a: 1}}] + io.handler({type: "result", text: "", toolCalls: native}) + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.toolCalls).toBe(native) + expect(lib.parseToolCalls).not.toHaveBeenCalled() + }) + + it("does not parse prose when no tools were offered", async () => { + await spec.handle({op: "generate", messages: []}, io) + io.handler({type: "result", text: '{"name":"x"}', toolCalls: null}) + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.toolCalls).toBeNull() + expect(lib.parseToolCalls).not.toHaveBeenCalled() + }) + + it("forwards only sampling overrides, never provider / apiKey / model / url", async () => { + await spec.handle( + { + op: "generate", + messages: [], + temperature: 0.2, + topP: 0.9, + provider: "openrouter", + apiKey: "sk-evil", + model: "gpt", + url: "https://attacker", + config: {apiKey: "sk-evil"}, + }, + io + ) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.overrides).toEqual({temperature: 0.2, topP: 0.9, topk: undefined, maxNewTokens: undefined}) + expect(Object.keys(opts.overrides)).not.toContain("provider") + expect(Object.keys(opts.overrides)).not.toContain("apiKey") + }) + + it("applies the provider-conditional system map and toolToggles before preparing", async () => { + lib = fakeLib({provider: "local", toolToggles: {off: false, optin: true}}) + spec = makeLLMWorkerSpec(lib) + await spec.handle( + { + op: "generate", + messages: [], + system: {default: "d", local: "L"}, + tools: [{name: "on"}, {name: "off"}, {name: "optin", defaultOff: true}, {name: "hidden", defaultOff: true}], + }, + io + ) + const [, opts] = lib.prepareGenerate.mock.calls[0] + expect(opts.system).toBe("L") + expect(opts.tools.map((t) => t.name)).toEqual(["on", "optin"]) + }) + + it("posts the library-built payload tagged with the transport's workerId", async () => { + const token = await spec.handle({op: "generate", messages: [{role: "user", content: "hi"}], sessionKey: "s1"}, io) + expect(token).toEqual({sessionKey: "s1"}) + expect(io.post).toHaveBeenCalledWith( + expect.objectContaining({type: "generate", id: "w1", sessionKey: "s1", messages: [{role: "user", content: "hi"}]}) + ) + }) + + it("runs builtin in-realm and still fills toolCalls", async () => { + lib = fakeLib({provider: "builtin", toolToggles: {}}) + lib.builtinGenerate = vi.fn(async () => "{}") + spec = makeLLMWorkerSpec(lib) + await spec.handle({op: "generate", messages: [], tools: [{name: "x"}]}, io) + await flush() + expect(io.post).not.toHaveBeenCalled() + const result = io.emit.mock.calls.map((c) => c[0]).find((f) => f.type === "result") + expect(result.text).toBe("{}") + expect(result.toolCalls).toEqual([{name: "x", args: {}}]) + }) +}) diff --git a/llm-host/tsconfig.json b/llm-host/tsconfig.json new file mode 100644 index 00000000..4696f12f --- /dev/null +++ b/llm-host/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNEXT", + "module": "ESNEXT", + "moduleResolution": "bundler", + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} diff --git a/llm-host/vite.config.ts b/llm-host/vite.config.ts new file mode 100644 index 00000000..886e68f2 --- /dev/null +++ b/llm-host/vite.config.ts @@ -0,0 +1,26 @@ +import {defineConfig} from "vite" +import solidPlugin from "vite-plugin-solid" +import patchworkBundles from "@chee/patchwork-bundles/vite" +import external from "@inkandswitch/patchwork-bootloader/externals" + +export default defineConfig({ + base: "./", + // Rewrite the automerge:-versioned @chee/patchwork-llm dep to a shared + // service-worker URL resolved at runtime (cross-origin), so the LLM lib loads + // as one canonical copy. This tool runs HOST-only, so loading patchwork-llm + // here is fine. (The tray injects its own