@aibranch/llm-meta-widget 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Embeddable browser chat widget for the [llm_meta](https://github.com/pubannotation) ecosystem.
4
4
 
5
- Client-orchestrated: the widget fetches host-side action schemas + host-published `.well-known/mcp.json` manifests at boot, dispatches tool_calls locally (page-embedded actions) or directly to MCP endpoints (host-wide well-known), and consumes the meta-server's SSE `single_llm_calls` API. Ships one Rails helper + one view partial + a served orchestrator JS module.
5
+ Client-orchestrated: the widget fetches host-side action schemas + host-published `.well-known/mcp.json` manifests at boot, dispatches tool_calls locally (page-embedded actions) or directly to MCP endpoints (host-wide well-known), and consumes the meta-server's SSE `single_llm_calls` API. Ships as a custom element in one self-contained ES module — on npm for any host, and in a Rails gem whose helper serves the identical file.
6
6
 
7
7
  **No** Devise, DB migrations, ChatManager, or PromptNavigator. Adds only `rails >= 8.0` as a runtime dep — so hosts that haven't bumped to 8.1 can adopt it without a Rails upgrade.
8
8
 
@@ -32,7 +32,7 @@ step, no Node at runtime.
32
32
 
33
33
  ```ruby
34
34
  # Gemfile
35
- gem "llm_meta_widget", "~> 0.4"
35
+ gem "llm_meta_widget", "~> 0.7"
36
36
  ```
37
37
 
38
38
  ```
@@ -62,8 +62,8 @@ the role label turns into a spinning gear reading "Working…", and the reply
62
62
  streams in.
63
63
 
64
64
  If the panel opens but nothing comes back, it is almost always CORS. The hub
65
- must list your app's origin in its `config/initializers/cors.rb` for the
66
- `/api/*` resource, because the widget calls the hub **from the visitor's
65
+ must list your app's origin in its `CORS_ORIGINS` environment variable for
66
+ the `/api/*` resource, because the widget calls the hub **from the visitor's
67
67
  browser**, not from your server. Open the browser console: a blocked request
68
68
  says so explicitly.
69
69
 
@@ -248,7 +248,7 @@ By default the widget renders two pickers below the input textarea:
248
248
  - **Model dropdown** — populated from the hub's `GET /api/llms` (anon path returns Ollama-only, since the widget's LLM calls use `api_key_uuid: "ollama-local"`).
249
249
  - **Tool picker** — populated from the hub's `GET /api/mcp_servers` (anon path returns `public_to_anonymous: true` servers). Two-level UX: server bulk-toggle + individual tool checkboxes on expand.
250
250
 
251
- Both pickers require **CORS**: the meta-server must include the host's origin in its `config/initializers/cors.rb` allowlist (`/api/*` resource). Without CORS, the fetch is silently blocked and pickers stay empty.
251
+ Both pickers require **CORS**: the meta-server must allow the host's origin on its `/api/*` resource. That allowlist is an environment variable, not code — `CORS_ORIGINS`, a comma-separated list — so adding a host is a deployment change and a restart, with no commit and nothing about one deployment's hosts published in the repo. Without CORS the fetch is silently blocked and the pickers stay empty.
252
252
 
253
253
  To adjust picker behavior at the helper call site:
254
254
 
@@ -257,8 +257,8 @@ To adjust picker behavior at the helper call site:
257
257
  llm_url: "https://your-meta-server.example",
258
258
  tool_hub_url: "https://your-meta-server.example",
259
259
  model: "qwen3-6-35b-fast", # initial selection
260
- enable_model_picker: true, # false → hide picker, use fixed `model:`
261
- enable_tool_picker: true, # false → hide picker, no Class-1 tools
260
+ enable_model_picker: true, # false → omit picker, use fixed `model:`
261
+ enable_tool_picker: true, # false → omit picker, no Class-1 tools
262
262
  models: nil, # nil = all anon models; ["qwen3-6-35b-fast", …] = allowlist
263
263
  hub_tools: nil # nil = all anon-public MCPs; ["togomcp", …] = allowlist by server name
264
264
  ) %>
@@ -289,8 +289,8 @@ To lock the widget to the fixed `model:` prop and disable Class-1 tools entirely
289
289
  | `well_known_urls:` | `nil` | `nil` = auto-discover same-origin; explicit array = fetch those; `[]` = disable |
290
290
  | `greeting:` | `nil` | First thing a visitor sees when the panel opens, above the offered prompt templates. `nil` = a generic line |
291
291
  | `max_rounds:` | `3` | Cap on tool-call rounds per LLM turn. Page actions cost a round each since 0.4.0 — raise it for multi-step flows |
292
- | `enable_model_picker:` | `true` | Show model dropdown (Level-1) |
293
- | `enable_tool_picker:` | `true` | Show tool picker (Level-1) |
292
+ | `enable_model_picker:` | `true` | Show model dropdown (Level-1). `false` removes it from the DOM |
293
+ | `enable_tool_picker:` | `true` | Show tool picker (Level-1). `false` removes it from the DOM |
294
294
  | `models:` | `nil` | Model-name allowlist; `nil` = all anon-available |
295
295
  | `hub_tools:` | `nil` | MCP-server-name allowlist; `nil` = all anon-public |
296
296
 
@@ -404,6 +404,10 @@ it most, since they must already know your form to press it.
404
404
  three tool classes plus prompts and resources. Its
405
405
  `app/views/annotation/text_annotation.html.erb` and `app/controllers/mcp_controller.rb`
406
406
  are the fullest example available.
407
+ - **Shipping the element** — `docs/custom-element-distribution.md` is the design note
408
+ behind the custom-element move: the npm package and its CDN URL, why `main`,
409
+ `exports` and `sideEffects` are set the way they are, and why the gem and the npm
410
+ package must never drift in version.
407
411
  - **Issues and questions** — <https://github.com/jdkim/llm_meta_widget/issues>
408
412
 
409
413
  ## License
@@ -1,8 +1,10 @@
1
1
  # Design note — distributing the widget as a custom element
2
2
 
3
- **Status:** implemented 2026-10-03 (same day), except for npm/CDN
4
- publication, which needs an npm account. Written as a proposal; kept as the
5
- record of why the shape is what it is.
3
+ **Status:** implemented and published 2026-10-03. The element shipped in 0.7.0,
4
+ npm/CDN publication followed the same day, and 0.7.2 fixed the one regression the
5
+ move introduced — see the correction below. Written as a proposal and kept as the
6
+ record of why the shape is what it is, with outcomes marked inline rather than
7
+ rewritten away.
6
8
  **Problem owner:** adopters whose host application is not Rails.
7
9
 
8
10
  **What landed, and what did not**
@@ -14,9 +16,12 @@ runs), a route and controller action serving the bundle, and the partial
14
16
  reduced to the element tag. The gem keeps its helper signature, so
15
17
  PubDictionaries needs no edit.
16
18
 
17
- Not landed: the npm package and the CDN URL, so the two-line integration in
18
- the example below currently means "load the file the gem serves, or
19
- self-host it". Everything else about it is true today.
19
+ Landed since: the npm package and the CDN URL. `@aibranch/llm-meta-widget` is on
20
+ npm, and `https://cdn.jsdelivr.net/npm/@aibranch/llm-meta-widget@0.7` resolves to
21
+ the bundle, so the two-line integration in the example below is now literally
22
+ true — no gem, no Rails and no self-hosting required. The gem and the npm package
23
+ ship the identical file, which is checked rather than assumed: both 0.7.2 copies
24
+ are sha1 `82d448cdd51fb81a`.
20
25
 
21
26
  Verified on a plain HTML page served by `python3 -m http.server`, with no
22
27
  Rails anywhere: the element upgrades and builds its own DOM, injects its own
@@ -32,6 +37,24 @@ a visitor changes model, and nothing before the browser would catch it. And
32
37
  the 985 lines of panel logic contain no ERB at all, which is why they moved
33
38
  verbatim rather than being rewritten.
34
39
 
40
+ **Correction — the markup did contain ERB (shipped broken, fixed in 0.7.2).**
41
+ That second finding was true of the panel's JavaScript and false of its HTML.
42
+ The template carried six `<% if enable_model_picker %>`-style tags plus the tail
43
+ of an ERB comment. Moved verbatim into a JS template literal they became inert
44
+ text, so they rendered as visible characters in the panel *and* the two picker
45
+ flags they implemented silently stopped being honoured — a disabled tools picker
46
+ rendered anyway. It shipped in 0.7.0, shipped again in 0.7.1, and was found by
47
+ eye in a screenshot rather than by any test. 0.7.2 removes the ERB and prunes the
48
+ pickers in `boot()` instead, which is where that conditionality has to live once
49
+ the template is static. `markup.test.mjs` now fails on any `<%` or `%>` in the
50
+ source or the built bundle, and the plain-host e2e run reports any template
51
+ syntax visible in the rendered panel.
52
+
53
+ The lesson is narrower than "grep for ERB". Moving a template between two
54
+ languages that both use angle brackets fails *silently*, because the receiving
55
+ language has no opinion about the sending language's delimiters — there is no
56
+ parse error to catch, only characters that happen to be inert.
57
+
35
58
  ## Why
36
59
 
37
60
  The widget is a Rails engine. Integration means `bundle add llm_meta_widget`, a
@@ -204,6 +227,30 @@ exists:
204
227
  3. Later releases run from Actions with `permissions: id-token: write`, no
205
228
  stored token. GitHub-hosted runners only; self-hosted is unsupported.
206
229
 
230
+ All three are done: 0.7.0 was published by hand, the Trusted Publisher is
231
+ configured, and 0.7.2 was the first release published end-to-end from a tag with
232
+ no credential anywhere. Two things each cost a release and are easy to miss:
233
+
234
+ - **Tick `npm publish` under Allowed actions.** `npm stage publish` is always
235
+ allowed and cannot be switched off, so a configuration with only that looks
236
+ permissive while quietly routing every release into staged publishing, which
237
+ waits for a maintainer to approve it with 2FA. `npm dist-tag` is not needed:
238
+ the implicit `latest` assignment is part of the publish operation.
239
+ - **Do not give `actions/setup-node` a `registry-url`.** With no token supplied
240
+ it still exports the literal placeholder `NODE_AUTH_TOKEN=XXXXX-XXXXX-XXXXX-XXXXX`
241
+ and writes it into a step `.npmrc`. npm sends that credential instead of
242
+ performing the OIDC exchange, and the registry answers `E404 … could not be
243
+ found or you do not have permission`, which reads like a missing package rather
244
+ than an auth failure. Trusted publishing wants no credential configured at all.
245
+
246
+ Diagnosing either needs `npm publish --loglevel verbose`: npm reports a failed
247
+ exchange only at verbose level before falling back to token auth, so the visible
248
+ symptom is a bare `ENEEDAUTH`. Two more things that look like failures and are
249
+ not — a successful publish answers `PUT 202`, not 201, and takes roughly 90
250
+ seconds to appear, so an empty packument straight after a green run means
251
+ nothing; and a version-conflict error proves nothing about auth, because npm
252
+ raises it from the public packument GET before any authenticated request.
253
+
207
254
  Note the asymmetry: the trusted publisher keys on the **GitHub** repo, which
208
255
  lives under `jdkim/`, while the npm scope is `@aibranch`. That works — they are
209
256
  different namespaces — but it is worth knowing before someone goes looking for
@@ -215,11 +262,15 @@ fail on a missing interpreter. `npm test` runs both locally.
215
262
 
216
263
  ## Risks
217
264
 
218
- **The refactor touches the least-tested half of the codebase.** The 107 node
219
- tests cover `orchestrator.js`. The panel has ESLint and the browser e2e runs,
220
- nothing more — and this moves 1,454 lines of it. Add node tests for attribute
221
- parsing (including the two traps above) and element construction *before*
222
- moving any code.
265
+ **The refactor touches the least-tested half of the codebase.** At the time the
266
+ node tests covered `orchestrator.js` only; the panel had ESLint and the browser
267
+ e2e runs, nothing more — and this moved 1,454 lines of it. Attribute-parsing
268
+ tests were written before the move as this said to (`config.test.mjs`), and the
269
+ suite now stands at 131. This risk still landed, though, because what went
270
+ untested was the *markup*: `element.js` cannot be imported headlessly — its CSS
271
+ imports need esbuild's loader — so the template had no unit coverage of any kind
272
+ until `markup.test.mjs`, and the ERB regression above walked straight through the
273
+ gap.
223
274
 
224
275
  **Version sync.** The gem version and the npm version must not drift. Either one
225
276
  release process drives both, or the gem pins an exact npm version. Two
@@ -244,5 +295,6 @@ one static asset.
244
295
  - Should the element emit DOM events (`llm-widget:turn-complete`, etc.) so hosts
245
296
  can react without reaching into internals? Cheap to add at build time,
246
297
  awkward to retrofit later.
247
- - npm package name: `llm-meta-widget` is free to check, and should match the
248
- gem name for discoverability.
298
+ - ~~npm package name~~ — **resolved:** `@aibranch/llm-meta-widget`. Scoped to the
299
+ `aibranch` org rather than matching the bare gem name; see Publishing to npm
300
+ for why that scope and not `@pubann`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aibranch/llm-meta-widget",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
4
4
  "//version": "Must stay in step with lib/llm_meta_widget/version.rb. Two independently versioned copies of the same client is the drift docs/custom-element-distribution.md warns about: bump both, release both.",
5
5
  "description": "Embeddable browser chat widget: a custom element that discovers tools at page, origin and hub scope and dispatches each tool call to the right one. Two lines of HTML on any host, Rails or not.",
6
6
  "keywords": [