wizards 0.5.0 → 0.5.1

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.
Files changed (158) hide show
  1. package/README.md +45 -42
  2. package/apps/runtime/dist/agents/studio-assistant.js +3 -3
  3. package/apps/runtime/dist/app.js +7 -4
  4. package/apps/runtime/dist/auth/index.js +2 -2
  5. package/apps/runtime/dist/authoring/guide.js +1 -1
  6. package/apps/runtime/dist/cli/account.js +31 -28
  7. package/apps/runtime/dist/cli/autostart.js +5 -3
  8. package/apps/runtime/dist/cli/clients.js +1 -1
  9. package/apps/runtime/dist/cli/connect.js +24 -24
  10. package/apps/runtime/dist/cli/features.js +6 -6
  11. package/apps/runtime/dist/cli/main.js +51 -39
  12. package/apps/runtime/dist/cli/mcp.js +5 -4
  13. package/apps/runtime/dist/cli/menu.js +32 -30
  14. package/apps/runtime/dist/cli/report.js +7 -9
  15. package/apps/runtime/dist/cli/setup.js +42 -38
  16. package/apps/runtime/dist/cli/start.js +16 -15
  17. package/apps/runtime/dist/cli/status.js +20 -20
  18. package/apps/runtime/dist/cli/ui.js +9 -2
  19. package/apps/runtime/dist/cli/update.js +6 -6
  20. package/apps/runtime/dist/cli/validate.js +1 -1
  21. package/apps/runtime/dist/connectors/external.js +3 -2
  22. package/apps/runtime/dist/connectors/index.js +5 -5
  23. package/apps/runtime/dist/connectors/plugin-connections.js +2 -2
  24. package/apps/runtime/dist/connectors/via-account.js +5 -5
  25. package/apps/runtime/dist/db/control-schema.js +14 -0
  26. package/apps/runtime/dist/db/migrations/control/0009_plugin_claim.sql +9 -0
  27. package/apps/runtime/dist/db/migrations/control/meta/0009_snapshot.json +561 -0
  28. package/apps/runtime/dist/db/migrations/control/meta/_journal.json +7 -0
  29. package/apps/runtime/dist/docs/dev/index.md +16 -14
  30. package/apps/runtime/dist/docs/dev/plugins/connections.md +27 -24
  31. package/apps/runtime/dist/docs/dev/plugins/events.md +19 -17
  32. package/apps/runtime/dist/docs/dev/plugins/examples.md +13 -12
  33. package/apps/runtime/dist/docs/dev/plugins/index.md +29 -24
  34. package/apps/runtime/dist/docs/dev/plugins/layout.md +20 -19
  35. package/apps/runtime/dist/docs/dev/plugins/quick-start.md +16 -15
  36. package/apps/runtime/dist/docs/dev/plugins/reference.md +52 -40
  37. package/apps/runtime/dist/docs/dev/plugins/reloading.md +24 -23
  38. package/apps/runtime/dist/docs/dev/plugins/routes.md +26 -22
  39. package/apps/runtime/dist/docs/dev/plugins/runners.md +49 -44
  40. package/apps/runtime/dist/docs/dev/plugins/server.md +95 -43
  41. package/apps/runtime/dist/docs/dev/plugins/shipping.md +19 -16
  42. package/apps/runtime/dist/docs/dev/plugins/space.md +75 -69
  43. package/apps/runtime/dist/docs/dev/plugins/starters.md +22 -17
  44. package/apps/runtime/dist/docs/dev/plugins/studio.md +80 -37
  45. package/apps/runtime/dist/docs/dev/plugins/styling.md +44 -41
  46. package/apps/runtime/dist/docs/dev/plugins/tables.md +11 -11
  47. package/apps/runtime/dist/docs/dev/plugins/tools.md +26 -25
  48. package/apps/runtime/dist/docs/dev/wizard-format.md +35 -23
  49. package/apps/runtime/dist/docs/user/ai-apps.md +46 -43
  50. package/apps/runtime/dist/docs/user/assistant.md +41 -36
  51. package/apps/runtime/dist/docs/user/build-a-wizard.md +64 -39
  52. package/apps/runtime/dist/docs/user/command-line.md +47 -44
  53. package/apps/runtime/dist/docs/user/connectors.md +40 -35
  54. package/apps/runtime/dist/docs/user/create-account.md +27 -22
  55. package/apps/runtime/dist/docs/user/data.md +29 -29
  56. package/apps/runtime/dist/docs/user/first-start.md +26 -28
  57. package/apps/runtime/dist/docs/user/index.md +16 -17
  58. package/apps/runtime/dist/docs/user/install.md +28 -27
  59. package/apps/runtime/dist/docs/user/local-only.md +21 -17
  60. package/apps/runtime/dist/docs/user/models.md +92 -84
  61. package/apps/runtime/dist/docs/user/plugins.md +28 -23
  62. package/apps/runtime/dist/docs/user/run.md +51 -50
  63. package/apps/runtime/dist/docs/user/server.md +9 -9
  64. package/apps/runtime/dist/docs/user/space.md +87 -65
  65. package/apps/runtime/dist/docs/user/steps.md +73 -69
  66. package/apps/runtime/dist/docs/user/test-and-share.md +127 -105
  67. package/apps/runtime/dist/docs/user/troubleshooting.md +40 -38
  68. package/apps/runtime/dist/docs/user/wordpress.md +62 -54
  69. package/apps/runtime/dist/documents/parse.js +2 -2
  70. package/apps/runtime/dist/engenty/shims/web-ingest.js +17 -1
  71. package/apps/runtime/dist/engine/input.js +4 -4
  72. package/apps/runtime/dist/engine/push.js +2 -2
  73. package/apps/runtime/dist/engine/runner.js +8 -8
  74. package/apps/runtime/dist/engine/steps.js +2 -2
  75. package/apps/runtime/dist/engine/surface.js +1 -1
  76. package/apps/runtime/dist/film/step.js +3 -3
  77. package/apps/runtime/dist/harness/claude.js +1 -1
  78. package/apps/runtime/dist/harness/codex.js +1 -1
  79. package/apps/runtime/dist/harness/model.js +2 -2
  80. package/apps/runtime/dist/harness/terminal.js +3 -3
  81. package/apps/runtime/dist/mcp/handler.js +2 -2
  82. package/apps/runtime/dist/mcp/tools.js +9 -9
  83. package/apps/runtime/dist/models.js +15 -15
  84. package/apps/runtime/dist/plugins/claims.js +56 -0
  85. package/apps/runtime/dist/plugins/loader.js +2 -0
  86. package/apps/runtime/dist/plugins/public.js +2 -3
  87. package/apps/runtime/dist/plugins/runs.js +13 -8
  88. package/apps/runtime/dist/render/chromium.js +24 -11
  89. package/apps/runtime/dist/routes/api.js +15 -9
  90. package/apps/runtime/dist/routes/debug.js +1 -1
  91. package/apps/runtime/dist/routes/delivery.js +46 -0
  92. package/apps/runtime/dist/routes/lang.js +7 -0
  93. package/apps/runtime/dist/routes/marketplace.js +2 -1
  94. package/apps/runtime/dist/routes/plugin-public.js +2 -1
  95. package/apps/runtime/dist/routes/plugins.js +13 -14
  96. package/apps/runtime/dist/routes/runs.js +59 -49
  97. package/apps/runtime/dist/routes/studio.js +53 -23
  98. package/apps/runtime/dist/services/access.js +3 -3
  99. package/apps/runtime/dist/services/assistant-chats.js +1 -1
  100. package/apps/runtime/dist/services/brand.js +2 -2
  101. package/apps/runtime/dist/services/cloud.js +5 -4
  102. package/apps/runtime/dist/services/errors.js +2 -1
  103. package/apps/runtime/dist/services/knowledge-model.js +2 -2
  104. package/apps/runtime/dist/services/marketplace.js +2 -2
  105. package/apps/runtime/dist/services/package-file.js +12 -9
  106. package/apps/runtime/dist/services/package.js +2 -2
  107. package/apps/runtime/dist/services/project-files.js +10 -12
  108. package/apps/runtime/dist/services/projects.js +13 -8
  109. package/apps/runtime/dist/services/runners.js +4 -3
  110. package/apps/runtime/dist/services/runs.js +3 -3
  111. package/apps/runtime/dist/services/site.js +2 -2
  112. package/apps/runtime/dist/services/space-categories.js +4 -4
  113. package/apps/runtime/dist/services/space-data.js +13 -13
  114. package/apps/runtime/dist/services/space-origin.js +1 -1
  115. package/apps/runtime/dist/services/spaces.js +7 -7
  116. package/apps/runtime/dist/services/wizards.js +5 -5
  117. package/apps/runtime/dist/tools/net-guard.js +135 -24
  118. package/apps/runtime/dist/tools/plugin.js +1 -1
  119. package/apps/runtime/dist/tools/store.js +1 -1
  120. package/apps/runtime/dist/widgets/step.js +1 -1
  121. package/apps/web/dist/assets/AboutDialog-DaYwk84X.js +1 -0
  122. package/apps/web/dist/assets/{ChangelogDialog-BXpkgatF.js → ChangelogDialog-CsYbEayb.js} +1 -1
  123. package/apps/web/dist/assets/{ChatView-t7z44uFN.js → ChatView-BRD3fKZU.js} +1 -1
  124. package/apps/web/dist/assets/{EditorPage-4qxNJgKr.js → EditorPage-ocPlZD3E.js} +3 -3
  125. package/apps/web/dist/assets/{FlowPreview-WpS8DCPl.js → FlowPreview-Bm7sv746.js} +1 -1
  126. package/apps/web/dist/assets/{Harness-DL7Bse40.js → Harness-DKE-YNA5.js} +1 -1
  127. package/apps/web/dist/assets/{NewWizardChannels-Crt6PzH2.js → NewWizardChannels-CnWckqjO.js} +1 -1
  128. package/apps/web/dist/assets/{NewWizardPage-COiVmNsl.js → NewWizardPage-BXTpbHVM.js} +1 -1
  129. package/apps/web/dist/assets/{OnboardingPage-BwluEqXq.js → OnboardingPage-3Rz5uOsa.js} +1 -1
  130. package/apps/web/dist/assets/{ProjectPage-BIRU_AlN.js → ProjectPage-QdSJwm3k.js} +1 -1
  131. package/apps/web/dist/assets/{PublicRunner-BxMb4gEt.js → PublicRunner-DEpBer5g.js} +1 -1
  132. package/apps/web/dist/assets/{ReadOnly-D9r5ze5S.js → ReadOnly-rr_RJD-a.js} +1 -1
  133. package/apps/web/dist/assets/{RunDrawer-DmJl2QOJ.js → RunDrawer-BsZt0tL9.js} +1 -1
  134. package/apps/web/dist/assets/{RunnerView-CF_1rC-7.js → RunnerView-BbRukEdE.js} +1 -1
  135. package/apps/web/dist/assets/SettingsPage-qkWsQIMN.js +1 -0
  136. package/apps/web/dist/assets/{SetupPage-BGKtbS7j.js → SetupPage-PzvPPLfM.js} +1 -1
  137. package/apps/web/dist/assets/{SharePage-C4MqXKDA.js → SharePage-7vdDWG1n.js} +1 -1
  138. package/apps/web/dist/assets/changelog-BFdAYD28.js +1 -0
  139. package/apps/web/dist/assets/{esm-CsY8TkyE.js → esm-g0kaGp8t.js} +1 -1
  140. package/apps/web/dist/assets/{index-BjQvGQp_.js → index-BzPid1oQ.js} +4 -4
  141. package/apps/web/dist/assets/{lock-CI6TpWL-.js → lock-BK9RYIft.js} +1 -1
  142. package/apps/web/dist/assets/oss-credits-BJrR7BR1.js +1 -0
  143. package/apps/web/dist/assets/{phone-BrtQz-fG.js → phone-DtkY0NfU.js} +1 -1
  144. package/apps/web/dist/assets/{settings-menu-BtEKL72n.js → settings-menu-7EJQRFOi.js} +1 -1
  145. package/apps/web/dist/assets/{ui-4IaTD7LB.js → ui-B6S5hYPd.js} +3 -3
  146. package/apps/web/dist/assets/{upload-CWJAtPRo.js → upload-rYCAy7ox.js} +1 -1
  147. package/apps/web/dist/index.html +2 -2
  148. package/apps/web/dist/mcp/flow.html +3 -3
  149. package/bin/wizards.mjs +1 -1
  150. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +21 -2
  151. package/node_modules/@engenty-wizards/shared/dist/projects.js +1 -1
  152. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +48 -48
  153. package/node_modules/@engenty-wizards/shared/dist/run.js +48 -48
  154. package/package.json +5 -4
  155. package/apps/web/dist/assets/AboutDialog-A_ILyeue.js +0 -1
  156. package/apps/web/dist/assets/SettingsPage-3xSInE-A.js +0 -1
  157. package/apps/web/dist/assets/changelog-CM_GdIVI.js +0 -1
  158. package/apps/web/dist/assets/oss-credits-BQQX0kvk.js +0 -1
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  title: Runners
3
- description: A door a wizard runs through, beside the page and the chat — a call, a messenger, a kiosk — and the run API that drives runs from it.
4
- version: 0.4.4
3
+ description: A door a wizard runs through, beside the page and the chat (a call, a messenger, a kiosk), and the run API that drives runs from it.
4
+ version: 0.5.0
5
5
  ---
6
6
 
7
7
  A runner is a way to run a wizard. The runtime has four: the stepped page at `/w/<token>`, the
8
8
  chat at `/w/<token>/chat`, live voice at `/w/<token>/talk` and the video call at
9
- `/w/<token>/video`; the last two only where a speech model is set up. A plugin adds more. The
10
- owner picks them per wizard when it is made, and later under "Change" in the share dialog; the
11
- link opens the first page runner that is on, the others answer under their own address or from
12
- their own channel.
9
+ `/w/<token>/video`. The last two are there only where a speech model is set up. A plugin adds
10
+ more. The owner picks them per wizard when it is made, and later under “Change” in the share
11
+ dialog. The link opens the first page runner that is on; the others answer under their own
12
+ address or from their own channel.
13
13
 
14
14
  ## Register a runner
15
15
 
@@ -32,24 +32,25 @@ server.registerRunner({
32
32
 
33
33
  | | |
34
34
  |---|---|
35
- | `id` | Lower case, digits and `-`, starting with a letter, at most 24 characters. The sub-path of the wizard's address. Not one of the built-in runners (`steps`, `chat`, `talk`, `video`), `runs` or `icons`: such an id throws "is taken" |
35
+ | `id` | Lower case, digits and `-`, starting with a letter, at most 24 characters. The sub-path of the wizard’s address. Not one of the built-in runners (`steps`, `chat`, `talk`, `video`), `runs` or `icons`: such an id throws “is taken” |
36
36
  | `label` | `{ de, en }`: its name in German and English |
37
37
  | `kind` | `page`: the person opens an address. `channel`: the runner has an address of its own (a number, a bot) and drives runs from its webhooks |
38
38
  | `capabilities` | What the runner takes in and shows; see below |
39
- | `page` | A page runner's handler: what `/w/<token>/<id>` answers with. Nobody is signed in. A page runner without one throws when it is registered |
40
- | `problem` | `() => string \| { text, kind?, note?, plan? } \| null`, may be async: why the runner cannot be used in the current tenant right now ("Not connected: Settings → WhatsApp"), asked inside the tenant whenever the studio lists the runners. A text is something to set up: the share dialog's row says "Noch nicht eingerichtet" with "Einrichten". An object says its `kind`: `setup` (the same), `waiting` (asked for, waiting for someone else; the row has no button), `plan` (the tenant's plan does not have it; the row offers the plans, "`plan` ansehen"); `note` (`{ de, en }`) is what the row says instead ("Nummer angefragt", "Ab Pro") |
41
- | `address` | A channel runner: `(wizard: { id, title, token }) => { number?, keyword?, link? } \| null`, may be async. Where a person reaches that wizard: the number, the keyword that picks it there, and a link that opens the channel with the keyword written (`sms:…`, `https://wa.me/…`). The share dialog shows the keyword and number under the runner's name, copies the link (else the number) and offers the link as a QR code. Asked for each wizard that has the runner on; null while it is not set up |
39
+ | `page` | A page runner’s handler: what `/w/<token>/<id>` answers with. Nobody is signed in. A page runner without one throws when it is registered |
40
+ | `problem` | `() => string \| { text, kind?, note?, plan? } \| null`, may be async: why the runner cannot be used in the current tenant right now (“Not connected: Settings → WhatsApp”). The runtime asks it inside the tenant whenever the studio lists the runners, and a runner with a problem can’t be switched on. A text is something to set up: the share dialog’s row says “Not set up yet” and offers “Set up”. An object says its `kind`: `setup` (the same), `waiting` (asked for and waiting for someone else; the row says “Waiting” and has no button) or `plan` (the tenant’s plan does not have it; the row says “Not in your plan” and offers “See {plan}” with the `plan` it names, else “See the plans”). `note` (`{ de, en }`) is what the row says instead (“Number requested”, “From Pro”) |
41
+ | `address` | A channel runner: `(wizard: { id, title, token }) => { number?, keyword?, link? } \| null`, may be async. Where a person reaches that wizard: the number, the keyword that picks it there, and a link that opens the channel with the keyword written (`sms:…`, `https://wa.me/…`). The share dialog shows the keyword and number under the runner’s name, copies the link (else the number) and offers the link as a QR code. Asked for each wizard that has the runner on; null while it is not set up |
42
42
 
43
- A page runner is served at `/w/<token>/<id>` for every wizard that switched it on. From
44
- source, where Vite serves the pages, the same handler answers at
45
- `/api/public/wizards/<token>/runners/<id>`; the public wizard's `runners` list carries the
43
+ A page runner is served at `/w/<token>/<id>` for every wizard that switched it on. When you run
44
+ from source, where Vite serves the pages, the same handler answers at
45
+ `/api/public/wizards/<token>/runners/<id>`; the public wizard’s `runners` list carries the
46
46
  right address either way.
47
47
 
48
48
  ### Capabilities
49
49
 
50
- The runner declares; the runtime works out per wizard how far it gets, and the editor shows it:
51
- everything in the channel, hands off to the page at these steps, or not possible. A kind the
52
- runtime does not know throws when the runner is registered.
50
+ The runner declares what it can do. The runtime works out per wizard how far it gets:
51
+ everything in the channel, a hand-off to the run page at some steps, or not possible. Where it is
52
+ not possible, the share dialog’s row says “Not possible:” with the steps. A kind the runtime does
53
+ not know throws when the runner is registered.
53
54
 
54
55
  | Field | Says |
55
56
  |---|---|
@@ -58,12 +59,12 @@ runtime does not know throws when the runner is registered.
58
59
  | `output.pictures` | What it shows as a picture with a link to the real thing. Anything else is a link |
59
60
  | `asks` | `confirm` for a running step that asks before it changes something; `login` only where a browser types the sign-in |
60
61
  | `review` | `accept`, `regenerate`, `edit` |
61
- | `waits` | Whether it stays through a step that takes minutes. A runner that cannot gets "not possible" for such wizards |
62
+ | `waits` | Whether it stays through a step that takes minutes. A runner that can’t is not possible for such wizards |
62
63
  | `handoff` | How a link to the run page reaches the person: `screen`, `thread`, `sms`, `push`. A runner with none cannot hand off |
63
64
 
64
65
  ## Drive a run
65
66
 
66
- `server.runs` holds the functions behind the MCP tools, for the plugin's own door. A run
67
+ `server.runs` holds the functions behind the MCP tools, for the plugin’s own door. A run
67
68
  started here is a live run of the published wizard for a person of that door.
68
69
 
69
70
  ```ts
@@ -89,17 +90,17 @@ server.registerPublicRoute({
89
90
 
90
91
  | Call | |
91
92
  |---|---|
92
- | `runs.start({ token \| wizardId, person, answers?, runner? })` | Starts a live run. `person` names who it is for on the door; the runtime keeps a hash of it as the run's visitor, so what the wizard remembers is theirs on the run page too. `answers` fills the first page; what did not fit comes back as `refused`. Gives the run's ticket |
93
- | `runs.report(runId, { waitSeconds?, draft? })` | The run as it stands: `lang`, `waitingFor` (a page with the fields shown and what they know, a review, a question), outputs with signed links and a `picture` (a PNG of a document, a dashboard or a widget), cost, `browserUrl`. Waits up to 45 seconds while the run works. A door that asks a page one field at a time passes what it has as `draft`: a field's `shown` then follows those answers, as the form on the screen does |
93
+ | `runs.start({ token \| wizardId, person, answers?, runner? })` | Starts a live run. `person` names who it is for on the door; the runtime keeps a hash of it as the run’s visitor, so what the wizard remembers is theirs on the run page too. `answers` fills the first page; what did not fit comes back as `refused`. Gives the run’s ticket |
94
+ | `runs.report(runId, { waitSeconds?, draft? })` | The run as it stands: `lang`, `waitingFor` (a page with the fields shown and what they know, a review, a question), outputs with signed links and a `picture` (a PNG of a document, a dashboard or a widget), cost, `browserUrl`. Waits up to 45 seconds while the run works. A door that asks a page one field at a time passes what it has as `draft`: a field’s `shown` then follows those answers, as the form on the screen does |
94
95
  | `runs.wizards(runner)` | The published wizards of the tenant that switched the runner on: what the door offers, with `token`, `title` and `lang` |
95
96
  | `runs.answerPage(runId, stepId, values)` | Fills the page the run waits for |
96
97
  | `runs.review(runId, stepId, action)` | Accepts a review, or asks for one of the shown steps again with a note |
97
- | `runs.answerAsk(runId, askId, "allow" \| "skip")` | Answers a running step's question. A sign-in cannot be answered here: hand off |
98
+ | `runs.answerAsk(runId, askId, "allow" \| "skip")` | Answers a running step’s question. A sign-in cannot be answered here: hand off |
98
99
  | `runs.control(runId, "back" \| "retry" \| "cancel")` | |
99
100
  | `runs.subscribe(runId, listener)` | Hears every change of the run: its events with their note in both languages, or a status change. Returns the function that stops it |
100
- | `runs.handoffUrl(runId)` | The run page with the run's ticket: the link to send for what the door cannot do. The person who opens it and the door's person are one to the wizard |
101
+ | `runs.handoffUrl(runId)` | The run page with the run’s ticket: the link to send for what the door cannot do. The person who opens it and the door’s person are one to the wizard |
101
102
  | `runs.upload(runId, { data, mime, name })` | A file of the person; the asset id goes into an image or file field with `answerPage` |
102
- | `runs.transcribe(runId, { data, mime })` | What is said in a recording of the person (a voice message), written down with the tenant's audio model and charged to the run. Up to 10 MB |
103
+ | `runs.transcribe(runId, { data, mime })` | What is said in a recording of the person (a voice message), written down with the tenant’s audio model and charged to the run. Up to 10 MB |
103
104
 
104
105
  A refused command throws an error with a `status` and a `code` (`not_found`, `invalid`,
105
106
  `refused`, `no_credits`); an `invalid` one also carries the fields that did not fit as
@@ -107,12 +108,12 @@ A refused command throws an error with a `status` and a `code` (`not_found`, `in
107
108
 
108
109
  ## What the person sees
109
110
 
110
- - The wizard's first page offers the runners that are on beside "Let's go".
111
+ - The wizard’s first page offers the runners that are on beside “Let’s go”.
111
112
  - A hand-off is a link to the same run; nothing is asked twice. The run page knows the
112
- door's person through the run, and "What this wizard remembers" is theirs there too.
113
- - The space's results carry the door a run came through.
113
+ door’s person through the run, and “What this wizard remembers” is theirs there too.
114
+ - The space’s results carry the door a run came through.
114
115
 
115
- ## The runner's part of the share dialog
116
+ ## The runner’s part of the share dialog
116
117
 
117
118
  The row of a channel runner, with its number, keyword, link and QR code, comes from its
118
119
  `address`. What the owner changes per wizard, such as the keyword, the studio half registers as
@@ -132,8 +133,8 @@ go into a [settings section](./studio.md).
132
133
 
133
134
  ### Setting up inside the dialog
134
135
 
135
- What "Einrichten" in a row opens: without more, the plugin's settings section. A setup the studio
136
- half registers opens inside the dialog instead, as a view with a back arrow, at the dialog's
136
+ “Set up” in a row opens the plugin’s settings section, unless the studio half registers a
137
+ setup. That setup opens inside the dialog instead, as a view with a back arrow, at the dialog’s
137
138
  height:
138
139
 
139
140
  ```tsx
@@ -149,21 +150,21 @@ function Setup({ wizard, Actions, done }: StudioShareSetupProps) {
149
150
  <>
150
151
  <p>…</p>
151
152
  <Actions>
152
- <Button onClick={() => ask.mutate()}>Nummer anfragen</Button>
153
+ <Button onClick={() => ask.mutate()}>{t("request")}</Button>
153
154
  </Actions>
154
155
  </>
155
156
  );
156
- // done({ title: "Nummer angefragt", text: "…", tone: "waiting" }) goes back.
157
+ // done({ title: t("requested"), text: "…", tone: "waiting" }) goes back.
157
158
  }
158
159
  ```
159
160
 
160
161
  | | |
161
162
  |---|---|
162
- | `title` | The view's heading, a function: the language can change |
163
- | `component` | Gets `{ wizard: { id, title }, Actions, done }`. What it renders inside `Actions` goes to the foot of the dialog, where the dialog's own buttons are. `done(banner?)` goes back to the main view and asks the rows again; `banner` (`{ title, text?, tone? }`, `tone` `done` green or `waiting` plain) shows on top |
164
- | `notice` | Optional: a banner above the rows while the runner has news ("Deine Nummer ist da"), drawn by the plugin, nothing while it has none. Gets `{ wizard, open }`; `open()` opens the setup. The dialog shows the first one of the runners that are on |
163
+ | `title` | The view’s heading, a function: the language can change |
164
+ | `component` | Gets `{ wizard: { id, title }, Actions, done }`. What it renders inside `Actions` goes to the foot of the dialog, where the dialog’s own buttons are. `done(banner?)` goes back to the main view and asks the rows again; `banner` (`{ title, text?, tone? }`, `tone` `done` green or `waiting` plain) shows on top |
165
+ | `notice` | Optional: a banner above the rows while the runner has news (“Your number is here”), drawn by the plugin. It draws nothing while there is no news. It gets `{ wizard, open }`; `open()` opens the setup. The dialog shows the banner of the first runner that is on |
165
166
 
166
- One setup per runner. A setup that asks the server for something calls the plugin's own routes;
167
+ One setup per runner. A setup that asks the server for something calls the plugin’s own routes;
167
168
  the rows show what `problem` answers after `done`.
168
169
 
169
170
  ## A thread door from the SDK
@@ -180,7 +181,7 @@ const surface: ThreadSurface = {
180
181
  runner: "sms",
181
182
  asks: ["text", "number", "select", "multiselect", "toggle", "date", "email", "url", "location"],
182
183
  async send(thread, prompt) {
183
- // prompt.kind: "text" | "choice" | "location" | "link" | "media" — draw it as the channel allows.
184
+ // prompt.kind: "text" | "choice" | "location" | "link" | "media": draw it as the channel allows.
184
185
  await twilio.send(thread.id, toText(prompt, thread.state.lang));
185
186
  },
186
187
  };
@@ -194,20 +195,24 @@ const door = new ThreadDoor({ runs: server.runs, surface, offered, watch, unwatc
194
195
  | `ThreadSurface.asks` | The field kinds the thread asks itself; a page with any other field goes to the screen as a link |
195
196
  | `send(thread, prompt)` | A `choice` is drawn as buttons, a list or numbered lines; a typed number or title counts as a tap, the door works that out. `link` and `media` carry a URL |
196
197
  | `media(ref)` | A file the person sent, by what the webhook named; without it files hand off |
197
- | `mayDeliver`, `reach` | A messenger with a window (WhatsApp's 24 hours): whether a free message may go out now, and the link by other means when not |
198
+ | `mayDeliver`, `reach` | A messenger with a window (WhatsApp’s 24 hours): whether a free message may go out now, and the link by other means when not |
198
199
  | `offered()` | The wizards switched on for the runner (`runs.wizards`) with their keywords; a keyword in the thread starts its wizard, else the door offers the menu |
199
200
  | `watch(thread)`, `unwatch(runId)` | Hear the run (`runs.subscribe`) and call `door.resume` when it moves; the plugin keeps the subscriptions |
200
201
  | `Thread` | `{ id, name, runId, state, lastInboundAt, lastMessageId }`: the plugin keeps it in a table per person and hands it to the door before and after |
201
202
 
203
+ Once a wizard runs, the door speaks the wizard’s language. Before that, it speaks the language
204
+ all offered wizards share; when they differ, it keeps the language it spoke last, which is
205
+ English in a new thread.
206
+
202
207
  A messenger plugin is a surface of this door: the webhook on a public route, the keyword table,
203
- the number and the keyword in the share dialog are the plugin's; buttons and lists, or numbered
208
+ the number and the keyword in the share dialog are the plugin’s; buttons and lists, or numbered
204
209
  lines and links, are its surface.
205
210
 
206
211
  ## A call
207
212
 
208
- A call has no thread and no screen: the person and a realtime speech model talk, and the
209
- model fills the run through tools, as the chat's live voice does in the browser. The runtime
210
- hands a plugin the tenant's speech model for it:
213
+ A call has no thread and no screen: the person talks with a realtime speech model, and the
214
+ model fills the run through tools, as the chat’s live voice does in the browser. The runtime
215
+ hands a plugin the tenant’s speech model for it:
211
216
 
212
217
  ```ts
213
218
  const { apiKey, model } = await server.speech.conversation();
@@ -215,9 +220,9 @@ const { apiKey, model } = await server.speech.conversation();
215
220
  ```
216
221
 
217
222
  A call plugin keeps the audio out of the runtime: the telephony provider hands the call to the
218
- speech model's SIP endpoint with a token in a header, the model's provider posts the incoming
219
- call to the plugin's public route, and the plugin accepts it with the wizard's rules and tools
220
- and answers the model's tool calls over a WebSocket, driving the run with `server.runs`. What a
223
+ speech model’s SIP endpoint with a token in a header, the model’s provider posts the incoming
224
+ call to the plugin’s public route, and the plugin accepts it with the wizard’s rules and tools
225
+ and answers the model’s tool calls over a WebSocket, driving the run with `server.runs`. What a
221
226
  voice cannot take goes out as a link.
222
227
 
223
228
  ## Not there yet
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: The server half
3
3
  description: The function a plugin exports, what it is handed, and the rules it runs under.
4
- version: 0.4.4
4
+ version: 0.5.0
5
5
  ---
6
6
 
7
7
  The server half is a file whose default export is a function. The runtime calls it once when the
@@ -29,7 +29,8 @@ export default definePlugin((wizards) => {
29
29
  | `wizards.server` | Registers tools, routes, tables and listeners. See the table below |
30
30
  | `wizards.plugin` | The manifest: `id`, `name`, `version` and whatever else it holds |
31
31
  | `wizards.config.get(key)` | A setting of the install: a line of its `.env`. Empty and missing are both `undefined` |
32
- | `wizards.log.info`, `.warn`, `.error` | A log line with the plugin's name in front |
32
+ | `wizards.features.has(name)`, `.all()` | The feature switches of the tenant at hand. See [Feature switches](#feature-switches) |
33
+ | `wizards.log.info`, `.warn`, `.error` | A log line with the plugin’s name in front |
33
34
  | `wizards.resolvePath(relative)` | A file of the plugin as an absolute path |
34
35
 
35
36
  | `wizards.server.` | Does | Page |
@@ -37,32 +38,33 @@ export default definePlugin((wizards) => {
37
38
  | `registerTool(tool)` | A tool for AI steps | [Tools](./tools.md) |
38
39
  | `registerSpaceContext(context)` | A block and tools for every AI step of a space | [The space](./space.md) |
39
40
  | `registerAssistantTool(tool)` | A tool of the space assistant | [The space](./space.md#tools-of-the-space-assistant) |
40
- | `registerAssistantContext(context)` | What the space assistant is told on the plugin's own page | [The space](./space.md#the-assistant-on-the-plugins-own-page) |
41
- | `spaceData.putPage(input)`, `putTable`, `putFile`, `putDocument`, `putCategories`, … | Knowledge of a space: pages, tables, files and Kategorien | [The space](./space.md#knowledge-a-plugin-writes) |
42
- | `spaces.brand(space)` | The space's look: its title, colours and fonts | [The space](./space.md#the-spaces-brand) |
43
- | `index.put(entry)`, `index.remove(space, key?)` | Texts in the space's search index | [The space](./space.md#the-search-index) |
41
+ | `registerAssistantContext(context)` | What the space assistant is told on the plugin’s own page | [The space](./space.md#the-assistant-on-the-plugins-own-page) |
42
+ | `spaceData.putPage(input)`, `putTable`, `putFile`, `putDocument`, `putCategories`, … | Knowledge of a space: pages, tables, files and categories | [The space](./space.md#knowledge-a-plugin-writes) |
43
+ | `spaces.brand(space)` | The space’s look: its title, colours and fonts | [The space](./space.md#the-spaces-brand) |
44
+ | `index.put(entry)`, `index.remove(space, key?)` | Texts in the space’s search index | [The space](./space.md#the-search-index) |
44
45
  | `registerHttpRoute(route)` | A route below `/api/studio/plugins/<id>` | [Routes](./routes.md) |
45
46
  | `registerPublicRoute(route)`, `publicUrl(path)` | A route anyone may call, and its address | [Routes](./routes.md#public-routes) |
46
47
  | `registerSharedRoute(route)`, `sharedUrl(path)`, `publicRef()` | One route for every tenant, and the ref that names one | [Routes](./routes.md#shared-routes) |
47
48
  | `channels.get()`, `request`, `withdraw`, `book` | The numbers the Manage-App gives the tenant | below |
49
+ | `claims.take(key)`, `put`, `drop`, `holder`, `mine` | Keys held across tenants, one tenant each | below |
48
50
  | `registerRunner(runner)` | A door a wizard runs through, beside the page and the chat | [Runners](./runners.md) |
49
- | `runs.start(…)`, `report`, `answerPage`, … | Drives runs from the plugin's own door | [Runners](./runners.md#drive-a-run) |
50
- | `speech.conversation()` | The tenant's realtime speech model, for a call | [Runners](./runners.md#a-call) |
51
- | `registerMigrations(folder)` | `.sql` files with the plugin's tables | [Tables](./tables.md) |
51
+ | `runs.start(…)`, `report`, `answerPage`, … | Drives runs from the plugin’s own door | [Runners](./runners.md#drive-a-run) |
52
+ | `speech.conversation()` | The tenant’s realtime speech model, for a call | [Runners](./runners.md#a-call) |
53
+ | `registerMigrations(folder)` | `.sql` files with the plugin’s tables | [Tables](./tables.md) |
52
54
  | `on(event, listener)` | Called when a run ends or a space changes | [Events](./events.md) |
53
55
  | `connections.available(…)`, `start`, `connect`, `list`, `call`, … | Accounts of a space, through the built-in connectors | [Accounts](./connections.md) |
54
56
  | `registerStarter(starter)` | A wizard the plugin brings as a template | [Starters](./starters.md) |
55
57
  | `every(name, everyMs, handler)` | A job, again and again | below |
56
58
  | `web.fetch(url, init?)`, `web.read(url)` | Requests to the web, guarded | below |
57
59
  | `documents.parse(file)` | A file as Markdown | below |
58
- | `secrets.seal(value)`, `secrets.unseal(sealed)` | Keys of the plugin's own, sealed for a table | below |
60
+ | `secrets.seal(value)`, `secrets.unseal(sealed)` | Keys of the plugin’s own, sealed for a table | below |
59
61
  | `getTenantDb()` | The database of the tenant the current request, step or event belongs to | [Tables](./tables.md) |
60
62
  | `generate(request)` | Asks a model of the current tenant | below |
61
63
  | `onUnload(fn)` | Something to undo when the plugin unloads | below |
62
64
 
63
65
  ## Settings
64
66
 
65
- Name a plugin's settings after the plugin and read them when the plugin loads:
67
+ Name a plugin’s settings after the plugin and read them when the plugin loads:
66
68
 
67
69
  ```ts
68
70
  const url = wizards.config.get("NOTIFY_URL");
@@ -72,17 +74,35 @@ if (!url) {
72
74
  }
73
75
  ```
74
76
 
75
- The person sets them in the install's `.env` and restarts. There is no settings page a plugin
76
- gets for free: a plugin that wants one adds a [settings section](./studio.md) and keeps the
77
- values in a [table](./tables.md).
77
+ The person sets them in the install’s `.env` and restarts engenty wizards. A plugin doesn’t get
78
+ a settings page for free: a plugin that wants one adds a [settings section](./studio.md) and
79
+ keeps the values in a [table](./tables.md).
80
+
81
+ ## Feature switches
82
+
83
+ A plugin declares its own switches in its manifest under `features`
84
+ ([Files and manifest](./layout.md#the-manifest)). Everywhere else each one is known as
85
+ `<plugin id>.<name>`: in `wizards features`, in the install’s `.features` file and in the
86
+ `FEATURES` setting.
87
+
88
+ ```ts
89
+ if (await wizards.features.has("leadScoring")) {
90
+ server.registerTool(scoreLead);
91
+ }
92
+ ```
93
+
94
+ - `has(name)` with a short name asks for the plugin’s own switch (`contacts.leadScoring`); a
95
+ full name asks for any switch, the runtime’s own included.
96
+ - `all()` gives every switch of the tenant at hand, by its full name.
97
+ - A switch is off unless something turned it on.
78
98
 
79
99
  ## Tenants
80
100
 
81
- A runtime serves one tenant on a person's computer and many on engenty.ai.
82
- Either way a plugin never names a tenant:
101
+ A runtime serves one tenant on a person’s computer and many on engenty.ai. Either way, a plugin
102
+ never names a tenant:
83
103
 
84
104
  - A route, a tool and a listener each run inside one tenant.
85
- - `server.getTenantDb()` is that tenant's database. Call it where you use it, not once at load
105
+ - `server.getTenantDb()` is that tenant’s database. Call it where you use it, not once at load
86
106
  time: at load time there is no tenant.
87
107
  - State kept in a variable of the module is shared by all tenants. Keep per-tenant state in a
88
108
  table.
@@ -91,7 +111,7 @@ Either way a plugin never names a tenant:
91
111
 
92
112
  On engenty.ai a team gets its number for texts and calls from engenty: it asks for one, an
93
113
  admin assigns it. `server.channels` reads it for the current tenant and books what it carries;
94
- the prices are the Manage-App's ([manage contract](https://github.com/engenty/wizards/blob/main/docs/manage-contract.md#numbers-of-a-tenant)).
114
+ the prices are the Manage-App’s ([manage contract](https://github.com/engenty/wizards/blob/main/docs/manage-contract.md#numbers-of-a-tenant)).
95
115
 
96
116
  ```ts
97
117
  const channels = await server.channels.get();
@@ -106,15 +126,46 @@ await server.channels.book({ kind: "sms", quantity: segments, key: messageSid, r
106
126
 
107
127
  - `get` is kept for a minute; `request` and `withdraw` forget it.
108
128
  - `request` throws `403` / `plan` on a plan without a number and `409` / `has_number` when
109
- the team has one. Asking again answers the open request.
110
- - `book`: `sms` by segment, sent or received, `call` by started minute. The same `key` books
111
- once.
129
+ the team has one. Asking again gives the open request.
130
+ - `book`: `sms` by segment, sent or received, `call` by started minute, `whatsapp` by
131
+ message sent. The same `key` books once.
112
132
  - A runtime without a Manage-App has no numbers: `get` gives `null`, `request` and `withdraw`
113
133
  throw `404` / `not_managed`, `book` books nothing.
114
134
 
135
+ ## Keys across tenants
136
+
137
+ What a [shared route](./routes.md#shared-routes) needs to find its tenant: a keyword on a number
138
+ every tenant shares, who last wrote to it, a number a tenant connected. `server.claims` holds
139
+ such keys, each for one tenant at a time, in the runtime’s own database:
140
+
141
+ ```ts
142
+ // In the tenant: the keyword is this tenant's, unless another one has it.
143
+ if (!(await server.claims.take(`kw:${keyword.toLowerCase()}`))) {
144
+ throw Object.assign(new Error("Taken."), { status: 409, code: "keyword_taken" });
145
+ }
146
+ await server.claims.put(`sender:${waId}`); // the last tenant they wrote to has them
147
+
148
+ // In the shared route: whose is it?
149
+ const ref = await server.claims.holder(`kw:${text.toLowerCase()}`);
150
+ if (ref) {
151
+ await inTenant(ref, () => handle(message));
152
+ }
153
+ ```
154
+
155
+ | | |
156
+ |---|---|
157
+ | `take(key)` | The current tenant takes the key; `false` when another tenant holds it |
158
+ | `put(key)` | The current tenant gets the key, whoever held it |
159
+ | `drop(key)` | The current tenant gives the key up; a key another tenant holds stays |
160
+ | `holder(key)` | The ref of the tenant that holds the key, for `inTenant`; `null` for none. Also outside a tenant |
161
+ | `mine(key)` | Whether the current tenant holds the key |
162
+
163
+ Keys are 1 to 200 characters and the plugin’s own: another plugin’s key of the same name is
164
+ another key. They stay when a tenant loses the plugin; `inTenant` then answers `404`.
165
+
115
166
  ## Asking a model
116
167
 
117
- `server.generate` asks one of the tenant's models, the same ones its wizards run on, and is paid
168
+ `server.generate` asks one of the tenant’s models, the same ones its wizards run on, and is paid
118
169
  like any other of its calls. With a `schema` the answer is an object of that shape:
119
170
 
120
171
  ```ts
@@ -132,13 +183,13 @@ const { object } = await server.generate({
132
183
  | `system` | How it should answer | |
133
184
  | `schema` | A zod schema: `object` is the answer in that shape | text only |
134
185
  | `model` | `classifier` (fast, cheap), `standard`, `high`, `highest` | `standard` |
135
- | `maxOutputTokens` | A limit on the answer | the model's |
186
+ | `maxOutputTokens` | A limit on the answer | the model’s |
136
187
  | `signal` | Aborts the call | two minutes |
137
188
  | `files` | Pictures (PNG, JPEG, WebP, GIF) and PDFs the model looks at with the prompt: `{ data, mediaType, name? }[]`, `data` a `Uint8Array` | none |
138
189
 
139
190
  It answers `{ text, object }`. Without a model set up it throws an error with the status `503`
140
191
  and the code `no_model`: a route that lets it through answers just that, and the studio half
141
- says it in the person's language.
192
+ says it in the person’s language.
142
193
 
143
194
  ## Jobs
144
195
 
@@ -151,11 +202,11 @@ server.every("refresh", 60 * 60_000, async ({ tenantId, lastRun, signal }) => {
151
202
  ```
152
203
 
153
204
  - The handler runs for every tenant that has the plugin, inside that tenant, at most once per
154
- `everyMs`. A minute is the least.
155
- - A job's name is lower case, digits and `_`, once per plugin.
156
- - When it ran is kept: a restart does not start the count again. A runtime that was off runs a
157
- due job once when it starts, not once per turn it missed. A local install runs jobs while it
158
- runs.
205
+ `everyMs`. The shortest interval is a minute.
206
+ - A job’s name is lower case, digits and `_`, once per plugin.
207
+ - The runtime keeps when a job ran, so a restart does not start the count again. A runtime that
208
+ was off runs a due job once when it starts, not once per turn it missed. On a person’s computer,
209
+ jobs run while engenty wizards runs.
159
210
  - One turn per tenant at a time. A turn that throws is logged and counts as run.
160
211
  - `lastRun` is when the turn before started; `null` the first time. `signal` is aborted
161
212
  when the plugin unloads.
@@ -172,20 +223,20 @@ const res = await server.web.fetch(`${origin}/sitemap.xml`, {
172
223
  });
173
224
  ```
174
225
 
175
- - Both refuse addresses of this machine and of the local network, after a redirect too, as the
176
- runtime's own requests do.
177
- - `read` gives the page's text as Markdown and its links, absolute and each once (up to
226
+ - Both refuse addresses of the computer the runtime runs on and of its local network, after a
227
+ redirect too, as the runtime’s own requests do.
228
+ - `read` gives the page’s text as Markdown and its links, absolute and each once (up to
178
229
  2,000). A page with hardly any text without its scripts is drawn in a browser first. `etag`
179
230
  and `lastModified` are what the server sent, for asking later whether the page changed. `jsonld`
180
- is the page's structured data (its `application/ld+json` blocks, parsed): a shop's product
231
+ is the page’s structured data (its `application/ld+json` blocks, parsed): a shop’s product
181
232
  page says title, price, availability and image there exactly, without a model.
182
233
  - `fetch` is `fetch` behind the guard: for a sitemap, `robots.txt`, a question with
183
234
  `if-none-match`.
184
235
 
185
- ## Keys of the plugin's own
236
+ ## Keys of the plugin’s own
186
237
 
187
238
  A plugin that talks to a shop or a service with keys the person typed keeps them sealed, as the
188
- runtime seals a connection's tokens:
239
+ runtime seals a connection’s tokens:
189
240
 
190
241
  ```ts
191
242
  await db().update(store).set({ credentials: server.secrets.seal({ token }) });
@@ -200,8 +251,9 @@ const keys = server.secrets.unseal<{ token: string }>(row.credentials);
200
251
  const { markdown, pages } = await server.documents.parse({ data, name: "preise.pdf", mime });
201
252
  ```
202
253
 
203
- PDF and Word files by their own text, scans and photos by a vision model, spreadsheets and CSV as
204
- tables. What was read is kept by content: the same bytes are read, and paid for, once.
254
+ PDF and Word files are read by their own text, scans and photos by a vision model, and
255
+ spreadsheets and CSV files as tables. What was read is kept by content, so the same bytes are
256
+ read, and paid for, only once.
205
257
 
206
258
  ## Undo on unload
207
259
 
@@ -217,13 +269,13 @@ The functions run last-first when the plugin unloads or loads again.
217
269
 
218
270
  ## When loading fails
219
271
 
220
- A plugin whose function throws keeps nothing of what it registered before the error: no half
221
- plugins. Settings → Plugins shows the error, and the plugin's routes answer `404` until its file
222
- is mended.
272
+ A plugin whose function throws keeps nothing of what it registered before the error, so there
273
+ are no half-loaded plugins. Settings → Plugins shows the error, and the plugin’s routes answer
274
+ `404` until you fix its file.
223
275
 
224
276
  ## Imports
225
277
 
226
278
  `@engenty-wizards/plugin-sdk` (and `@engenty-wizards/plugin-sdk/thread`), `zod`, `drizzle-orm` and
227
- `drizzle-orm/sqlite-core` come from the
228
- runtime's own copies, so a schema or a table a plugin makes is one the runtime understands. See
229
- [Files and manifest](./layout.md#dependencies) for everything else.
279
+ `drizzle-orm/sqlite-core` come from the runtime’s own copies, so a schema or a table a plugin
280
+ makes is one the runtime understands. See [Files and manifest](./layout.md#dependencies) for
281
+ everything else.
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  title: Shipping
3
3
  description: Hand a plugin to someone else, put it into a server's image, and decide which tenant has it.
4
- version: 0.3.2
4
+ version: 0.5.0
5
5
  ---
6
6
 
7
- ## To someone's install
7
+ ## To someone’s install
8
8
 
9
- A plugin is a file or a folder. Whoever installs it puts it into their plugins folder:
9
+ A plugin is a file or a folder. Whoever installs it puts it into their plugins folder, which is
10
10
  `~/.engenty/wizards/plugins` for an installed app.
11
11
 
12
12
  Give them:
@@ -18,7 +18,7 @@ Give them:
18
18
  | Dependencies | A `node_modules` beside the plugin, if it imports anything but the SDK, `zod` and `drizzle-orm` |
19
19
  | Settings | The names of the `.env` lines it reads, in its description |
20
20
 
21
- Say what it does with data: a plugin runs with the app's full rights, and the person who
21
+ Say what the plugin does with data. A plugin runs with the app’s full rights, and the person who
22
22
  installs it trusts you with them.
23
23
 
24
24
  ## As a package
@@ -31,8 +31,8 @@ npm install @acme/wizards-contacts
31
31
  ```
32
32
 
33
33
  The runtime finds `node_modules/@acme/wizards-contacts/engenty.plugin.json` at its next start,
34
- or with "Look again" in Settings → Plugins. Put the manifest, the server half and the built
35
- `dist/` into the package's `files`.
34
+ or with “Look again” in Settings → Plugins. Put the manifest, the server half and the built
35
+ `dist/` into the package’s `files`.
36
36
 
37
37
  ## Into a server
38
38
 
@@ -47,17 +47,20 @@ services:
47
47
  - ./plugins:/plugins
48
48
  ```
49
49
 
50
- Or copy the plugins into an image of your own, built on the runtime's.
50
+ Or copy the plugins into an image of your own, built on the runtime’s.
51
51
 
52
52
  ## With engenty wizards itself
53
53
 
54
- Plugins in the repository's `modules/` folder ship with every runtime: in a checkout, in the
55
- installed app and in the Docker image. A module there is part of the open product.
54
+ Plugins in the repository’s `modules/` folder ship with every runtime: in a checkout, in the
55
+ npm package and installed app, and in the Docker image. A module there is part of the open
56
+ product.
56
57
 
57
- - Its server half imports only what the runtime hands every plugin and its own files. It has no
58
+ - Its server half imports only what the runtime hands every plugin, and its own files. It has no
58
59
  `node_modules` of its own where it is installed.
59
60
  - A module with a studio half is a workspace package whose `build` script is
60
- `node ../../scripts/build-plugin.mjs .`, so the product's build builds its `dist/`.
61
+ `node ../../scripts/build-plugin.mjs .`, so the product’s build builds its `dist/`.
62
+ `apps/runtime/Dockerfile` copies each workspace package’s `package.json` before it installs:
63
+ add the module’s there.
61
64
 
62
65
  A plugin that is not part of the open product stays out of `modules/` and goes into a folder of
63
66
  `PLUGINS_DIR`.
@@ -66,11 +69,11 @@ A plugin that is not part of the open product stays out of `modules/` and goes i
66
69
 
67
70
  On an install of your own, on your computer or your own server, every tenant has every plugin
68
71
  the runtime loaded. On engenty.ai the service installs the plugins and switches them on per
69
- account; a person cannot install one there.
72
+ account. A person cannot install a plugin there.
70
73
 
71
74
  ## Versions
72
75
 
73
- - `version` in the manifest is shown in the settings. The runtime does not compare versions.
74
- - A plugin's `.sql` files only ever grow: an update adds files, it never changes one that ran.
75
- - A wizard that uses a plugin's tool names it by id. Renaming a tool or the plugin breaks those
76
- wizards: keep ids for good.
76
+ - The settings show `version` from the manifest. The runtime doesn’t compare versions.
77
+ - A plugin’s `.sql` files only ever grow: an update adds files and never changes one that ran.
78
+ - A wizard that uses a plugin’s tool names it by its id. Renaming a tool or the plugin breaks
79
+ those wizards, so keep ids for good.