wizards 0.4.4 → 0.4.5

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 (120) hide show
  1. package/apps/runtime/dist/agents/architect.js +32 -13
  2. package/apps/runtime/dist/agents/subscription.js +12 -12
  3. package/apps/runtime/dist/app.js +9 -5
  4. package/apps/runtime/dist/assistant/knows.js +2 -2
  5. package/apps/runtime/dist/authoring/example.js +21 -20
  6. package/apps/runtime/dist/authoring/guide.js +4 -2
  7. package/apps/runtime/dist/connectors/imap.js +4 -4
  8. package/apps/runtime/dist/connectors/index.js +40 -15
  9. package/apps/runtime/dist/connectors/plugin-connections.js +3 -2
  10. package/apps/runtime/dist/connectors/via-account.js +8 -6
  11. package/apps/runtime/dist/docs/dev/plugins/connections.md +1 -1
  12. package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
  13. package/apps/runtime/dist/documents/parse.js +5 -3
  14. package/apps/runtime/dist/engine/events.js +1 -1
  15. package/apps/runtime/dist/engine/input.js +28 -8
  16. package/apps/runtime/dist/engine/prepare.js +2 -2
  17. package/apps/runtime/dist/engine/requirements.js +12 -3
  18. package/apps/runtime/dist/engine/runner.js +34 -23
  19. package/apps/runtime/dist/engine/steps.js +6 -5
  20. package/apps/runtime/dist/engine/surface.js +2 -1
  21. package/apps/runtime/dist/engine/template.js +1 -1
  22. package/apps/runtime/dist/engine/types.js +2 -1
  23. package/apps/runtime/dist/film/project.js +1 -1
  24. package/apps/runtime/dist/film/skills.js +2 -2
  25. package/apps/runtime/dist/film/sounds.js +1 -1
  26. package/apps/runtime/dist/film/step.js +7 -4
  27. package/apps/runtime/dist/film/tools.js +1 -1
  28. package/apps/runtime/dist/geocode.js +1 -1
  29. package/apps/runtime/dist/harness/apple.js +0 -5
  30. package/apps/runtime/dist/harness/claude.js +2 -1
  31. package/apps/runtime/dist/harness/codex.js +2 -1
  32. package/apps/runtime/dist/harness/model.js +4 -3
  33. package/apps/runtime/dist/harness/terminal.js +4 -3
  34. package/apps/runtime/dist/model-errors.js +8 -3
  35. package/apps/runtime/dist/models.js +58 -38
  36. package/apps/runtime/dist/plugins/runs.js +2 -1
  37. package/apps/runtime/dist/routes/api.js +16 -5
  38. package/apps/runtime/dist/routes/lang.js +13 -0
  39. package/apps/runtime/dist/routes/runs.js +12 -19
  40. package/apps/runtime/dist/routes/studio.js +40 -28
  41. package/apps/runtime/dist/services/access.js +4 -3
  42. package/apps/runtime/dist/services/architect.js +15 -4
  43. package/apps/runtime/dist/services/assistant-chats.js +2 -1
  44. package/apps/runtime/dist/services/brand.js +3 -2
  45. package/apps/runtime/dist/services/errors.js +7 -5
  46. package/apps/runtime/dist/services/knowledge-model.js +3 -2
  47. package/apps/runtime/dist/services/marketplace.js +6 -5
  48. package/apps/runtime/dist/services/package.js +10 -3
  49. package/apps/runtime/dist/services/profile.js +12 -0
  50. package/apps/runtime/dist/services/project-files.js +13 -12
  51. package/apps/runtime/dist/services/projects.js +11 -6
  52. package/apps/runtime/dist/services/runs.js +18 -11
  53. package/apps/runtime/dist/services/shares.js +2 -1
  54. package/apps/runtime/dist/services/site.js +21 -10
  55. package/apps/runtime/dist/services/space-categories.js +7 -6
  56. package/apps/runtime/dist/services/space-data.js +19 -16
  57. package/apps/runtime/dist/services/spaces.js +8 -7
  58. package/apps/runtime/dist/services/wizards.js +41 -20
  59. package/apps/runtime/dist/store/errors.js +2 -1
  60. package/apps/runtime/dist/store/index.js +21 -4
  61. package/apps/runtime/dist/store/shared-lists.js +2 -3
  62. package/apps/runtime/dist/tools/index.js +17 -2
  63. package/apps/runtime/dist/tools/plugin.js +3 -2
  64. package/apps/runtime/dist/widgets/render.js +1 -1
  65. package/apps/runtime/dist/widgets/step.js +2 -1
  66. package/apps/web/dist/assets/{AboutDialog-nLn3vZAN.js → AboutDialog-BCtnYRj2.js} +1 -1
  67. package/apps/web/dist/assets/{ChangelogDialog-Cbx3mN2p.js → ChangelogDialog-DBjoIIHL.js} +1 -1
  68. package/apps/web/dist/assets/ChatView-BcyUYN0Q.js +10 -0
  69. package/apps/web/dist/assets/EditorPage-Ce1QD-DK.js +3 -0
  70. package/apps/web/dist/assets/{FlowPreview-Bvj3R8g4.js → FlowPreview-BtpAdyt-.js} +1 -1
  71. package/apps/web/dist/assets/{Harness-D-Z3MbYq.js → Harness-DJJ_yMLY.js} +2 -2
  72. package/apps/web/dist/assets/NewWizardChannels-BrYLMV6F.js +1 -0
  73. package/apps/web/dist/assets/NewWizardPage-827zwhhW.js +7 -0
  74. package/apps/web/dist/assets/{OnboardingPage-CdoAfnI2.js → OnboardingPage-jC6FidpF.js} +1 -1
  75. package/apps/web/dist/assets/ProjectPage-BYcM1-1H.js +1 -0
  76. package/apps/web/dist/assets/{PublicRunner-CHwK3X75.js → PublicRunner-BofZl0MO.js} +1 -1
  77. package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-B4nIGkoj.js} +1 -1
  78. package/apps/web/dist/assets/RunDrawer-Dx-BRCkY.js +4 -0
  79. package/apps/web/dist/assets/RunnerView-CDCp61aq.js +1 -0
  80. package/apps/web/dist/assets/SettingsPage-B49nLHwv.js +1 -0
  81. package/apps/web/dist/assets/SetupPage-4tzzHUq0.js +1 -0
  82. package/apps/web/dist/assets/SharePage-Bs4s73h4.js +1 -0
  83. package/apps/web/dist/assets/changelog-D-12LwZB.js +1 -0
  84. package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BueS7qO8.js} +1 -1
  85. package/apps/web/dist/assets/esm-CjGdsKU4.js +7 -0
  86. package/apps/web/dist/assets/index-BRDn-5v_.js +14 -0
  87. package/apps/web/dist/assets/index-mlnAPOdf.css +1 -0
  88. package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-CkXYnQux.js} +1 -1
  89. package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CHkEf-Dz.js} +1 -1
  90. package/apps/web/dist/assets/settings-menu-CrVpArM_.js +1 -0
  91. package/apps/web/dist/assets/ui-DrdqFaQt.js +134 -0
  92. package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-CzkkZqAo.js} +1 -1
  93. package/apps/web/dist/index.html +4 -4
  94. package/apps/web/dist/mcp/flow.html +76 -76
  95. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +4 -2
  96. package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
  97. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
  98. package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
  99. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
  100. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
  101. package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
  102. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
  103. package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
  104. package/package.json +1 -1
  105. package/apps/web/dist/assets/ChatView-1VuHE2Q7.js +0 -10
  106. package/apps/web/dist/assets/EditorPage-Cp3jqQsk.js +0 -3
  107. package/apps/web/dist/assets/NewWizardChannels-DCF7JoL0.js +0 -1
  108. package/apps/web/dist/assets/NewWizardPage-BTlgq7yN.js +0 -7
  109. package/apps/web/dist/assets/ProjectPage-CoPSdDO9.js +0 -1
  110. package/apps/web/dist/assets/RunDrawer-CmGuqp0p.js +0 -4
  111. package/apps/web/dist/assets/RunnerView-DiaQiDbw.js +0 -1
  112. package/apps/web/dist/assets/SettingsPage-NzmFrJXb.js +0 -1
  113. package/apps/web/dist/assets/SetupPage-8mBuezdY.js +0 -1
  114. package/apps/web/dist/assets/SharePage-C-T5fkp1.js +0 -1
  115. package/apps/web/dist/assets/changelog-CsxPWKJ-.js +0 -1
  116. package/apps/web/dist/assets/esm-CnR8rDPi.js +0 -7
  117. package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
  118. package/apps/web/dist/assets/index-D57rKyGN.js +0 -14
  119. package/apps/web/dist/assets/settings-menu-D-UqnL1p.js +0 -1
  120. package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
@@ -7,7 +7,7 @@ const bus = new EventEmitter();
7
7
  bus.setMaxListeners(0);
8
8
  export async function emitEvent(runId, stepId, type, said, asset) {
9
9
  const note = typeof said === "string" ? null : said;
10
- const message = note ? noteText(note, "de") : said;
10
+ const message = note ? noteText(note, "en") : said;
11
11
  const [row] = await db
12
12
  .insert(schema.runEvent)
13
13
  .values({
@@ -1,4 +1,9 @@
1
1
  import { isDecidedField, isLocationValue, MAX_FILES, offeredSlot, shownFields, } from "@engenty-wizards/shared/definition";
2
+ import { bilingual, saidText } from "@engenty-wizards/shared/run";
3
+ /** The field errors in a language, for whoever reads them. */
4
+ export function fieldErrorsIn(errors, lang) {
5
+ return errors.map((e) => ({ field: e.field, message: saidText(e.message, lang) }));
6
+ }
2
7
  function empty(v) {
3
8
  return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
4
9
  }
@@ -137,36 +142,48 @@ export function readPageInput(step, input, context = {}) {
137
142
  continue;
138
143
  }
139
144
  if (field.required && empty(v) && field.kind !== "toggle") {
140
- errors.push({ field: field.id, message: "Pflichtfeld" });
145
+ errors.push({ field: field.id, message: bilingual("Pflichtfeld", "Required") });
141
146
  continue;
142
147
  }
143
148
  if (Array.isArray(v) && (field.kind === "image" || field.kind === "file")) {
144
149
  if (field.min && v.length < field.min) {
145
- errors.push({ field: field.id, message: `Bitte mindestens ${field.min} auswählen` });
150
+ errors.push({
151
+ field: field.id,
152
+ message: bilingual(`Bitte mindestens ${field.min} auswählen`, `Please choose at least ${field.min}`),
153
+ });
146
154
  continue;
147
155
  }
148
156
  if (field.max && v.length > field.max) {
149
157
  errors.push({
150
158
  field: field.id,
151
- message: `Höchstens ${field.max} – bitte einige entfernen`,
159
+ message: bilingual(`Höchstens ${field.max} – bitte einige entfernen`, `At most ${field.max} – please remove some`),
152
160
  });
153
161
  continue;
154
162
  }
155
163
  }
156
164
  if (field.kind === "number" && Number.isNaN(v)) {
157
- errors.push({ field: field.id, message: "Bitte eine Zahl eingeben" });
165
+ errors.push({
166
+ field: field.id,
167
+ message: bilingual("Bitte eine Zahl eingeben", "Please enter a number"),
168
+ });
158
169
  continue;
159
170
  }
160
171
  if (field.kind === "select" && !empty(v) && !field.options?.includes(String(v))) {
161
- errors.push({ field: field.id, message: "Ungültige Auswahl" });
172
+ errors.push({ field: field.id, message: bilingual("Ungültige Auswahl", "Invalid choice") });
162
173
  continue;
163
174
  }
164
175
  if (field.kind === "slot" && !empty(v) && !field.options?.includes(String(v))) {
165
- errors.push({ field: field.id, message: "Dieser Termin wird nicht angeboten" });
176
+ errors.push({
177
+ field: field.id,
178
+ message: bilingual("Dieser Termin wird nicht angeboten", "This time is not offered"),
179
+ });
166
180
  continue;
167
181
  }
168
182
  if (field.kind === "email" && !empty(v) && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(v))) {
169
- errors.push({ field: field.id, message: "Keine gültige E-Mail-Adresse" });
183
+ errors.push({
184
+ field: field.id,
185
+ message: bilingual("Keine gültige E-Mail-Adresse", "Not a valid email address"),
186
+ });
170
187
  continue;
171
188
  }
172
189
  if (field.kind === "url" && !empty(v)) {
@@ -174,7 +191,10 @@ export function readPageInput(step, input, context = {}) {
174
191
  new URL(String(v));
175
192
  }
176
193
  catch {
177
- errors.push({ field: field.id, message: "Keine gültige URL" });
194
+ errors.push({
195
+ field: field.id,
196
+ message: bilingual("Keine gültige URL", "Not a valid URL"),
197
+ });
178
198
  continue;
179
199
  }
180
200
  }
@@ -68,7 +68,7 @@ async function listenTo(ctx, id, label, value) {
68
68
  if (!found || found.row.runId !== ctx.runId || !/^(audio|video)\//.test(found.row.mime)) {
69
69
  return;
70
70
  }
71
- await ctx.emit("info", `Hört „${label}“ an …`);
71
+ await ctx.emit("info", { code: "listen", params: { label } });
72
72
  try {
73
73
  const { pieces, costUsd } = await listenPieces({
74
74
  bytes: new Uint8Array(found.data),
@@ -85,6 +85,6 @@ async function listenTo(ctx, id, label, value) {
85
85
  }
86
86
  // The step goes on without the words; the next one tries again.
87
87
  console.error(`[run ${ctx.runId}] listen ${id}`, err);
88
- await ctx.emit("info", `„${label}“ konnte nicht verschriftlicht werden.`);
88
+ await ctx.emit("info", { code: "listenFailed", params: { label } });
89
89
  }
90
90
  }
@@ -1,5 +1,6 @@
1
1
  import { conditionMatches, conditionsOf, isDecidedField, isDecisionStep, isOtherwise, MODEL_CLASSES, } from "@engenty-wizards/shared/definition";
2
2
  import { closedChoicesFor, openValue, otherwiseIndex, unavoidableFrom, unavoidableSteps, } from "@engenty-wizards/shared/fit";
3
+ import { bilingual, saidText } from "@engenty-wizards/shared/run";
3
4
  import { classProblem, filmClient } from "../models.js";
4
5
  // The steps every run passes through, and the choices that lead around a missing model, are
5
6
  // pure and shared (packages/shared/src/fit.ts): the standalone check uses the same.
@@ -220,8 +221,12 @@ export async function stepsWithoutModel(def) {
220
221
  const missing = await missingClasses();
221
222
  return new Set(def.steps.filter((s) => stepClasses(s).some((cls) => missing.has(cls))).map((s) => s.id));
222
223
  }
224
+ /** The classes this wizard needs that this runtime cannot serve, in a language; empty = every step can run. */
225
+ export async function missingModels(def, lang = "en") {
226
+ return (await missingModelsSaid(def)).map((m) => ({ ...m, problem: saidText(m.problem, lang) }));
227
+ }
223
228
  /** The classes this wizard needs that this runtime cannot serve; empty = every step can run. */
224
- export async function missingModels(def) {
229
+ export async function missingModelsSaid(def) {
225
230
  const unavoidable = unavoidableSteps(def);
226
231
  const needs = new Map();
227
232
  for (const step of def.steps) {
@@ -268,9 +273,13 @@ export function blockingMessage(missing) {
268
273
  if (!blocking.length) {
269
274
  return null;
270
275
  }
271
- return blocking
272
- .map((m) => `${m.steps.map((s) => `„${s.title}“`).join(", ")}: ${m.problem}`)
276
+ const line = (lang) => blocking
277
+ .map((m) => {
278
+ const steps = m.steps.map((s) => (lang === "de" ? `„${s.title}“` : `“${s.title}”`));
279
+ return `${steps.join(", ")}: ${saidText(m.problem, lang)}`;
280
+ })
273
281
  .join(" ");
282
+ return bilingual(line("de"), line("en"));
274
283
  }
275
284
  /** The class a marketplace capability stands for; the rest need no model of their own. */
276
285
  const CAPABILITY_CLASS = {
@@ -1,6 +1,6 @@
1
1
  import { marginOf, } from "@engenty-wizards/shared/decision";
2
2
  import { branchValues, cardsFromData, conditionsOf, dataRef, followRules, formatsFor, isDecidedField, LIST_FORMATS, listRef, loopKey, MAX_SHOWN_FIELDS, optionsFromData, otherwiseOf, pageNeeds, whenHolds, wizardLang, } from "@engenty-wizards/shared/definition";
3
- import { noteText, } from "@engenty-wizards/shared/run";
3
+ import { bilingual, saidText, } from "@engenty-wizards/shared/run";
4
4
  import { CONVERSATION_RUNNERS, DEFAULT_RUNNER_SETTINGS } from "@engenty-wizards/shared/runners";
5
5
  import { eq } from "drizzle-orm";
6
6
  import { nanoid } from "nanoid";
@@ -12,7 +12,7 @@ import { env } from "../env.js";
12
12
  import { saveAsset } from "../files/storage.js";
13
13
  import { managed, tenantInfo } from "../manage.js";
14
14
  import { hasFfmpeg } from "../media/ffmpeg.js";
15
- import { NotedError, noteOf } from "../model-errors.js";
15
+ import { NotedError, noteOf, saidOf } from "../model-errors.js";
16
16
  import { conversationAvailable, freeTierRefusal, freeTierText, ModelUnavailableError, noteFreeTierRefusal, } from "../models.js";
17
17
  import { runEnded } from "../plugins/events.js";
18
18
  import { projectFiles } from "../services/project-files.js";
@@ -26,7 +26,7 @@ import { clearDeviceCalls } from "./device.js";
26
26
  import { emitEvent, recentEvents, signalChanged } from "./events.js";
27
27
  import { readPageInput } from "./input.js";
28
28
  import { pushRun } from "./push.js";
29
- import { blockingMessage, closedChoices, missingModels, openValue, stepsWithoutModel, } from "./requirements.js";
29
+ import { blockingMessage, closedChoices, missingModelsSaid, openValue, stepsWithoutModel, } from "./requirements.js";
30
30
  import { releaseResources, resourcesFor } from "./resources.js";
31
31
  import { runAutomaticStep } from "./steps.js";
32
32
  import { answersAsText, resolveRef } from "./template.js";
@@ -41,9 +41,9 @@ export class RunInputError extends NotedError {
41
41
  this.errors = errors;
42
42
  }
43
43
  }
44
- export class NoCreditsError extends Error {
44
+ export class NoCreditsError extends NotedError {
45
45
  constructor() {
46
- super("Dieser Wizard hat gerade kein Guthaben mehr. Bitte später erneut versuchen.");
46
+ super(bilingual("Dieser Wizard hat gerade kein Guthaben mehr. Bitte später erneut versuchen.", "This wizard has no credits left right now. Please try again later."));
47
47
  }
48
48
  }
49
49
  /** The runs this process drives right now, with the handle that stops each. */
@@ -99,7 +99,7 @@ function stepOf(def, id) {
99
99
  }
100
100
  function friendly(err) {
101
101
  if (err instanceof StepError || err instanceof ModelUnavailableError) {
102
- return err.message;
102
+ return saidOf(err);
103
103
  }
104
104
  // An own AI Gateway key on Vercel's free tier: the person can top it up or pick another way.
105
105
  const refused = freeTierRefusal(err);
@@ -109,32 +109,32 @@ function friendly(err) {
109
109
  const msg = err?.message ?? String(err);
110
110
  // The model-gateway answers 402 once the tenant's credits are used up.
111
111
  if (/insufficient_credits|\b402\b/i.test(msg)) {
112
- return new NoCreditsError().message;
112
+ return saidOf(new NoCreditsError());
113
113
  }
114
114
  // A runtime that runs alone pays its provider itself: its admin can top the account up.
115
115
  if (!managed && /minimum balance|insufficient_funds|top up your credits/i.test(msg)) {
116
- return "Das Guthaben beim Modell-Anbieter reicht dafür nicht aus (Videos brauchen dort ein Mindestguthaben). Bitte dort aufladen und noch einmal versuchen.";
116
+ return bilingual("Das Guthaben beim Modell-Anbieter reicht dafür nicht aus (Videos brauchen dort ein Mindestguthaben). Bitte dort aufladen und noch einmal versuchen.", "The credit with the model provider is not enough for this (videos need a minimum balance there). Please top up there and try again.");
117
117
  }
118
118
  if (/rate.?limit|429/i.test(msg)) {
119
- return "Der KI-Dienst ist gerade ausgelastet. Bitte gleich noch einmal versuchen.";
119
+ return bilingual("Der KI-Dienst ist gerade ausgelastet. Bitte gleich noch einmal versuchen.", "The AI service is busy right now. Please try again in a moment.");
120
120
  }
121
121
  if (/timeout|timed out/i.test(msg)) {
122
- return "Der Schritt hat zu lange gedauert. Bitte noch einmal versuchen.";
122
+ return bilingual("Der Schritt hat zu lange gedauert. Bitte noch einmal versuchen.", "The step took too long. Please try again.");
123
123
  }
124
124
  // The platform's own provider account (budget, quota, key): nothing the person can fix.
125
125
  if (/budget|quota|insufficient|billing|credit balance|api key|unauthori[sz]ed|401|403/i.test(msg)) {
126
- return "Der KI-Dienst ist gerade nicht erreichbar. Bitte später noch einmal versuchen.";
126
+ return bilingual("Der KI-Dienst ist gerade nicht erreichbar. Bitte später noch einmal versuchen.", "The AI service cannot be reached right now. Please try again later.");
127
127
  }
128
- return `Dieser Schritt ist fehlgeschlagen: ${msg.slice(0, 300)}`;
128
+ return bilingual(`Dieser Schritt ist fehlgeschlagen: ${msg.slice(0, 300)}`, `This step failed: ${msg.slice(0, 300)}`);
129
129
  }
130
130
  /**
131
131
  * A step that failed ends the run. A failure with a note (Apple Intelligence's) goes into the log
132
132
  * as that note, so a runner shows it in its language; the run's error is in the wizard's.
133
133
  */
134
134
  async function failRun(run, stepId, err) {
135
- const note = noteOf(err);
136
- const message = note ? noteText(note, wizardLang(run.definition)) : friendly(err);
137
- await emitEvent(run.id, stepId, "error", note ?? message);
135
+ const said = noteOf(err) ?? friendly(err);
136
+ const message = saidText(said, wizardLang(run.definition));
137
+ await emitEvent(run.id, stepId, "error", said);
138
138
  await updateRun(run.id, { status: "failed", error: message });
139
139
  }
140
140
  // --- creating & driving ------------------------------------------------------
@@ -143,7 +143,7 @@ export async function createRun(input) {
143
143
  const first = input.definition.steps[0];
144
144
  const state = { values: {}, outputs: {}, history: [], notes: {} };
145
145
  // A step every run passes through needs a model this runtime cannot serve: refuse before anything is held.
146
- const blocked = blockingMessage(await missingModels(input.definition));
146
+ const blocked = blockingMessage(await missingModelsSaid(input.definition));
147
147
  if (blocked) {
148
148
  throw new ModelUnavailableError(blocked);
149
149
  }
@@ -219,7 +219,11 @@ async function take(runId, tenant) {
219
219
  await withTenant(tenant, () => drive(runId, abort.signal))
220
220
  .catch(async (err) => {
221
221
  console.error(`[run ${runId}]`, err);
222
- await withTenant(tenant, () => updateRun(runId, { status: "failed", error: friendly(err) })).catch(() => undefined);
222
+ await withTenant(tenant, async () => {
223
+ const run = await loadRun(runId);
224
+ const lang = run ? wizardLang(run.definition) : "en";
225
+ await updateRun(runId, { status: "failed", error: saidText(friendly(err), lang) });
226
+ }).catch(() => undefined);
223
227
  })
224
228
  .finally(async () => {
225
229
  clearInterval(beat);
@@ -631,14 +635,17 @@ export async function submitPage(runId, stepId, input) {
631
635
  const answer = values[id];
632
636
  if (answer !== undefined &&
633
637
  (choice.values.length === 0 || choice.values.some((v) => String(v) === String(answer)))) {
634
- errors.push({ field: id, message: "Das ist hier gerade nicht verfügbar." });
638
+ errors.push({
639
+ field: id,
640
+ message: bilingual("Das ist hier gerade nicht verfügbar.", "That is not available here right now."),
641
+ });
635
642
  }
636
643
  }
637
644
  if (errors.length) {
638
645
  throw new RunInputError(errors);
639
646
  }
640
647
  const connections = step.fields.some((f) => f.kind === "connection")
641
- ? await connectionViews(run.definition.connections ?? [], scopeOf(run), await projectIdOf(run))
648
+ ? await connectionViews(run.definition.connections ?? [], scopeOf(run), await projectIdOf(run), wizardLang(run.definition))
642
649
  : [];
643
650
  for (const field of step.fields) {
644
651
  if (field.kind === "image" || field.kind === "file") {
@@ -646,7 +653,9 @@ export async function submitPage(runId, stepId, input) {
646
653
  for (const id of Array.isArray(value) ? value : value ? [value] : []) {
647
654
  const asset = await db.query.asset.findFirst({ where: eq(schema.asset.id, String(id)) });
648
655
  if (asset?.runId !== runId) {
649
- throw new RunInputError([{ field: field.id, message: "Datei nicht gefunden" }]);
656
+ throw new RunInputError([
657
+ { field: field.id, message: bilingual("Datei nicht gefunden", "File not found") },
658
+ ]);
650
659
  }
651
660
  }
652
661
  }
@@ -657,7 +666,9 @@ export async function submitPage(runId, stepId, input) {
657
666
  values[field.id] = account.label;
658
667
  }
659
668
  else if (field.required) {
660
- throw new RunInputError([{ field: field.id, message: "Bitte zuerst verbinden" }]);
669
+ throw new RunInputError([
670
+ { field: field.id, message: bilingual("Bitte zuerst verbinden", "Please connect first") },
671
+ ]);
661
672
  }
662
673
  }
663
674
  }
@@ -701,7 +712,7 @@ export async function reviewStep(runId, stepId, action) {
701
712
  if (!step.show.includes(action.target)) {
702
713
  throw new RunConflict("Unknown target");
703
714
  }
704
- state.notes[action.target] = action.note.slice(0, 2000) || "Bitte eine neue Variante.";
715
+ state.notes[action.target] = action.note.slice(0, 2000) || "Please make a new variant.";
705
716
  // A step with several results makes only the named ones again.
706
717
  if (action.items?.length) {
707
718
  state.redo = { ...state.redo, [action.target]: action.items };
@@ -884,7 +895,7 @@ export async function runView(run, brand) {
884
895
  ask: run.status === "running" ? run.ask : null,
885
896
  lists,
886
897
  connections: step?.type === "page" && step.fields.some((f) => f.kind === "connection")
887
- ? await connectionViews(def.connections ?? [], scopeOf(run), await projectIdOf(run))
898
+ ? await connectionViews(def.connections ?? [], scopeOf(run), await projectIdOf(run), wizardLang(def))
888
899
  : [],
889
900
  keeps: Boolean(def.lists?.length || def.connections?.length),
890
901
  shareUrl: run.shareToken ? `${env.publicUrl}/s/${run.shareToken}` : null,
@@ -1,6 +1,7 @@
1
1
  import { pOf } from "@engenty-wizards/shared/decision";
2
2
  import { allFields, dataRef, isDecisionStep, LOCALES, templateRefs, wizardLang, } from "@engenty-wizards/shared/definition";
3
3
  import { mainLogo, ruleLines } from "@engenty-wizards/shared/projects";
4
+ import { bilingual } from "@engenty-wizards/shared/run";
4
5
  import { SURFACE_GUIDE, validateSurface, } from "@engenty-wizards/shared/surface";
5
6
  import { Agent } from "@mastra/core/agent";
6
7
  import { generateText, Output } from "ai";
@@ -378,7 +379,7 @@ function agentEntries(step, ctx) {
378
379
  async function runAgentEach(step, ctx) {
379
380
  const entries = agentEntries(step, ctx);
380
381
  if (!entries.length) {
381
- throw new StepError("Es gibt keine Einträge, für die dieser Schritt arbeiten könnte.");
382
+ throw new StepError(bilingual("Es gibt keine Einträge, für die dieser Schritt arbeiten könnte.", "There are no entries for this step to work on."));
382
383
  }
383
384
  // The view, if any, is composed once over the table of all entries.
384
385
  const single = {
@@ -553,7 +554,7 @@ export async function runAgentStep(step, ctx) {
553
554
  const text = step.output.format === "json" ? everything : closing || everything;
554
555
  if (step.output.format !== "json") {
555
556
  if (!text && !assets.length) {
556
- throw new StepError("Der Schritt hat kein Ergebnis geliefert.");
557
+ throw new StepError(bilingual("Der Schritt hat kein Ergebnis geliefert.", "The step did not deliver a result."));
557
558
  }
558
559
  return { text, assets: assets.length ? assets : undefined, at: new Date().toISOString() };
559
560
  }
@@ -754,7 +755,7 @@ async function writeHtml(step, ctx, kind) {
754
755
  .replace(/```\s*$/, "")
755
756
  .trim();
756
757
  if (!/<html|<!doctype/i.test(html)) {
757
- throw new StepError("Das Dokument konnte nicht erstellt werden.");
758
+ throw new StepError(bilingual("Das Dokument konnte nicht erstellt werden.", "The document could not be made."));
758
759
  }
759
760
  // On screen (preview, PNG) a print document needs the margin its @page rule gives it on paper.
760
761
  return kind === "document"
@@ -805,7 +806,7 @@ async function inParallel(items, limit, work) {
805
806
  async function runMediaStep(step, ctx, asset) {
806
807
  const entries = entriesOf(step, ctx);
807
808
  if (!entries.length) {
808
- throw new StepError("Für diesen Schritt fehlen die Vorlagen (Bilder oder Szenen).");
809
+ throw new StepError(bilingual("Für diesen Schritt fehlen die Vorlagen (Bilder oder Szenen).", "This step is missing its sources (images or scenes)."));
809
810
  }
810
811
  const previous = ctx.state.outputs[step.id];
811
812
  const before = previous?.assets?.filter((a) => a.kind === asset) ?? [];
@@ -897,7 +898,7 @@ export async function runGenerateStep(step, ctx) {
897
898
  case "voice": {
898
899
  const text = renderTemplate(step.prompt, ctx.scope).trim().slice(0, 4000);
899
900
  if (!text) {
900
- throw new StepError("Es gibt keinen Text zum Vorlesen.");
901
+ throw new StepError(bilingual("Es gibt keinen Text zum Vorlesen.", "There is no text to read aloud."));
901
902
  }
902
903
  const note = ctx.state.notes[step.id];
903
904
  const media = await generateSpeechMedia({
@@ -1,4 +1,5 @@
1
1
  import { assembleSurface, dataRef, listRef, } from "@engenty-wizards/shared/definition";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { validateSurface } from "@engenty-wizards/shared/surface";
3
4
  import { decide } from "./decide.js";
4
5
  import { answersAsText, resolveRef } from "./template.js";
@@ -13,7 +14,7 @@ export async function runSurfaceStep(step, ctx) {
13
14
  const components = step.components ?? (await decidedComponents(step, ctx));
14
15
  const issues = validateSurface(components, Object.keys(data));
15
16
  if (issues.length) {
16
- throw new StepError(`Die Ansicht ist fehlerhaft: ${issues.map((i) => i.message).join(" ")}`);
17
+ throw new StepError(bilingual(`Die Ansicht ist fehlerhaft: ${issues.map((i) => i.message).join(" ")}`, `The view is faulty: ${issues.map((i) => i.message).join(" ")}`));
17
18
  }
18
19
  return { surface: { components, data }, at: new Date().toISOString() };
19
20
  }
@@ -11,7 +11,7 @@ function money(n, currency, locale) {
11
11
  }
12
12
  }
13
13
  /** Line items as a Markdown table with their totals — what a model needs to write a document. */
14
- export function itemsAsMarkdown(field, value, values, lang = "de") {
14
+ export function itemsAsMarkdown(field, value, values, lang = "en") {
15
15
  const locale = LOCALES[lang];
16
16
  const totals = itemsTotals(field, value, values);
17
17
  const cols = field.columns ?? [];
@@ -1,3 +1,4 @@
1
+ import { NotedError } from "../model-errors.js";
1
2
  /** A step failure the person can read. */
2
- export class StepError extends Error {
3
+ export class StepError extends NotedError {
3
4
  }
@@ -250,7 +250,7 @@ export async function prepareFilmFolder(step, ctx) {
250
250
  const preset = { "16:9": "landscape", "9:16": "portrait", "1:1": "square" }[step.format];
251
251
  const init = await runHyperframes(["init", "film", "--non-interactive", "--skip-transcribe", `--resolution=${preset}`], { cwd: root, signal: ctx.signal, timeoutMs: 5 * 60_000 });
252
252
  if (init.code !== 0 || !existsSync(join(root, "film"))) {
253
- throw new Error(`Das Filmprojekt konnte nicht angelegt werden: ${init.out.slice(-300)}`);
253
+ throw new Error(`The film project could not be set up: ${init.out.slice(-300)}`);
254
254
  }
255
255
  await put(root, ".engenty/inputs", stamp);
256
256
  return { root, brief };
@@ -21,7 +21,7 @@ const fetching = new Map();
21
21
  export function platformSkills(name) {
22
22
  const pkg = PLATFORM_SKILLS[name];
23
23
  if (!pkg) {
24
- return Promise.reject(new Error(`Das Skill-Paket „${name}“ gibt es nicht.`));
24
+ return Promise.reject(new Error(`There is no skill package “${name}”.`));
25
25
  }
26
26
  const dir = join(env.dataDir, "skills", `${pkg.name}@${pkg.version.slice(0, 12)}`);
27
27
  if (existsSync(join(dir, ".complete"))) {
@@ -37,7 +37,7 @@ export function platformSkills(name) {
37
37
  async function fetchPackage(pkg, dir) {
38
38
  const res = await fetch(pkg.tarball);
39
39
  if (!res.ok) {
40
- throw new Error(`Das Skill-Paket „${pkg.name}“ konnte nicht geladen werden (${res.status}).`);
40
+ throw new Error(`The skill package “${pkg.name}” could not be loaded (${res.status}).`);
41
41
  }
42
42
  const tmp = `${dir}.part`;
43
43
  await rm(tmp, { recursive: true, force: true });
@@ -108,7 +108,7 @@ async function build() {
108
108
  }
109
109
  }
110
110
  if (!sounds.length) {
111
- throw new Error("Die Geräusche für Filme konnten nicht geladen werden.");
111
+ throw new Error("The sounds for films could not be loaded.");
112
112
  }
113
113
  sounds.sort((a, b) => a.file.localeCompare(b.file));
114
114
  await writeFile(join(tmp, "catalog.json"), JSON.stringify({
@@ -2,10 +2,12 @@ import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { readFile, rm, stat } from "node:fs/promises";
4
4
  import { isAbsolute, join, normalize } from "node:path";
5
+ import { bilingual } from "@engenty-wizards/shared/run";
5
6
  import { StepError } from "../engine/types.js";
6
7
  import { CLAUDE_BIN, claudeEnv } from "../harness/claude.js";
7
8
  import { failureOf } from "../harness/model.js";
8
9
  import { openBridge } from "../mcp/bridge.js";
10
+ import { errorText } from "../model-errors.js";
9
11
  import { filmClient } from "../models.js";
10
12
  import { prepareFilmFolder, savedSession, saveSession } from "./project.js";
11
13
  import { runHyperframes } from "./tools.js";
@@ -227,12 +229,13 @@ export async function runFilmStep(step, ctx) {
227
229
  if (ctx.signal.aborted) {
228
230
  throw err;
229
231
  }
230
- throw new StepError(`${client.name} hat beim Film aufgehört: ${err.message}. Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`);
232
+ throw new StepError(bilingual(`${client.name} hat beim Film aufgehört: ${err.message}. Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`, `${client.name} stopped working on the film: ${err.message}. The work is saved; a new attempt carries on from there.`));
231
233
  });
232
234
  const result = last;
233
235
  if (!result || result.is_error) {
234
- const detail = String(result?.result ?? (stderr.trim() || "kein Ergebnis")).slice(0, 400);
235
- throw new StepError(`${failureOf(client, detail).message} Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`);
236
+ const detail = String(result?.result ?? (stderr.trim() || "no result")).slice(0, 400);
237
+ const failure = failureOf(client, detail);
238
+ throw new StepError(bilingual(`${errorText(failure, "de")} Die Arbeit ist gespeichert; ein neuer Versuch macht dort weiter.`, `${errorText(failure, "en")} The work is saved; a new attempt carries on from there.`));
236
239
  }
237
240
  return result;
238
241
  };
@@ -262,7 +265,7 @@ export async function runFilmStep(step, ctx) {
262
265
  }
263
266
  const problem = renderProblem(render.out);
264
267
  if (attempt >= RENDER_FIXES) {
265
- throw new StepError(`Der Film ließ sich nicht rendern: ${problem.slice(0, 400)}`);
268
+ throw new StepError(bilingual(`Der Film ließ sich nicht rendern: ${problem.slice(0, 400)}`, `The film could not be rendered: ${problem.slice(0, 400)}`));
266
269
  }
267
270
  await ctx.emit("tool", { code: "filmFix" });
268
271
  const fixed = await turn(`The final render of film/ failed:\n\n${problem}\n\nFind the cause in the composition's code, fix it, run \`check\` and a draft render through the hyperframes tool until both pass, and end with one line on what you fixed.`, await savedSession(root));
@@ -40,7 +40,7 @@ async function install() {
40
40
  `hyperframes@${HYPERFRAMES_VERSION}`,
41
41
  ], { cwd: home, timeoutMs: 10 * 60_000 });
42
42
  if (code !== 0 || !existsSync(entry)) {
43
- throw new Error(`HyperFrames konnte nicht installiert werden: ${out.slice(-400)}`);
43
+ throw new Error(`HyperFrames could not be installed: ${out.slice(-400)}`);
44
44
  }
45
45
  return entry;
46
46
  }
@@ -19,7 +19,7 @@ export function placeLabel(result) {
19
19
  * The address of a position, when the server has a geocoder (`GEOCODER_URL`). Calls go out one
20
20
  * at a time and at most one a second — what a public Nominatim asks of its users.
21
21
  */
22
- export function reverseGeocode(lat, lng, language = "de") {
22
+ export function reverseGeocode(lat, lng, language = "en") {
23
23
  if (!env.geocoderUrl) {
24
24
  return Promise.resolve(null);
25
25
  }
@@ -2,7 +2,6 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { release } from "node:os";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { noteText } from "@engenty-wizards/shared/run";
6
5
  import { nanoid } from "nanoid";
7
6
  import { env } from "../env.js";
8
7
  import { ffmpeg, hasFfmpeg } from "../media/ffmpeg.js";
@@ -103,10 +102,6 @@ export function appleUnavailableNote(status) {
103
102
  return { code: "appleDevice" };
104
103
  }
105
104
  }
106
- /** What to tell the person when Apple Intelligence cannot answer, in German (the settings). */
107
- export function appleUnavailable(status) {
108
- return noteText(appleUnavailableNote(status), "de");
109
- }
110
105
  /**
111
106
  * The failure the helper reports, as the person should read it: a note, so a run shows it in the
112
107
  * wizard's language.
@@ -1,3 +1,4 @@
1
+ import { bilingual } from "@engenty-wizards/shared/run";
1
2
  import { resolveEnv, run } from "./env.js";
2
3
  import { failureOf, HarnessModel, lastJson, runClient, } from "./model.js";
3
4
  /**
@@ -142,5 +143,5 @@ export const claude = {
142
143
  },
143
144
  login: { args: ["auth", "login"], interactive: false },
144
145
  model: (alias, effort) => new ClaudeCodeModel(claude, alias || "sonnet", effort),
145
- exhausted: "Das Anthropic-Konto, mit dem Claude Code angemeldet ist, hat kein Guthaben. Melde Claude Code mit deinem Abo an: in den Einstellungen unter „Modelle“.",
146
+ exhausted: bilingual("Das Anthropic-Konto, mit dem Claude Code angemeldet ist, hat kein Guthaben. Melde Claude Code mit deinem Abo an: in den Einstellungen unter „Modelle“.", "The Anthropic account Claude Code is signed in with has no credit. Sign Claude Code in with your subscription: under “Models” in the settings."),
146
147
  };
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { bilingual } from "@engenty-wizards/shared/run";
4
5
  import { resolveEnv, run } from "./env.js";
5
6
  import { failureOf, HarnessImageModel, HarnessModel, runClient, } from "./model.js";
6
7
  /**
@@ -224,5 +225,5 @@ export const codex = {
224
225
  login: { args: ["login"], interactive: false },
225
226
  model: (alias, effort) => new CodexModel(codex, alias || "default", effort),
226
227
  image: { alias: "image", model: (alias) => new CodexImageModel(codex, alias || "image") },
227
- exhausted: "Das Kontingent des ChatGPT-Kontos, mit dem Codex angemeldet ist, ist aufgebraucht. Später noch einmal versuchen oder einen anderen Client wählen.",
228
+ exhausted: bilingual("Das Kontingent des ChatGPT-Kontos, mit dem Codex angemeldet ist, ist aufgebraucht. Später noch einmal versuchen oder einen anderen Client wählen.", "The quota of the ChatGPT account Codex is signed in with is used up. Try again later or pick another client."),
228
229
  };
@@ -1,16 +1,17 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
+ import { bilingual } from "@engenty-wizards/shared/run";
4
5
  import { nanoid } from "nanoid";
5
6
  import { env } from "../env.js";
6
7
  import { openBridge } from "../mcp/bridge.js";
7
8
  import { ModelUnavailableError } from "../model-errors.js";
8
9
  import { extensionOf, jsonInstruction, parseJsonAnswer, renderPrompt } from "./prompt.js";
9
10
  const workDir = join(env.dataDir, "harness");
10
- export const signedOut = (h) => `${h.name} ist auf diesem Gerät nicht angemeldet. Melde dich in den Einstellungen unter „Modelle“ an.`;
11
- export const notInstalled = (h) => `${h.name} ist auf diesem Gerät nicht installiert. Installiere es in den Einstellungen unter „Modelle“.`;
11
+ export const signedOut = (h) => bilingual(`${h.name} ist auf diesem Gerät nicht angemeldet. Melde dich in den Einstellungen unter „Modelle“ an.`, `${h.name} is not signed in on this device. Sign in under “Models” in the settings.`);
12
+ export const notInstalled = (h) => bilingual(`${h.name} ist auf diesem Gerät nicht installiert. Installiere es in den Einstellungen unter „Modelle“.`, `${h.name} is not installed on this device. Install it under “Models” in the settings.`);
12
13
  export const exhausted = (h) => h.exhausted ??
13
- `Das Konto, mit dem ${h.name} angemeldet ist, hat kein Guthaben oder Kontingent mehr.`;
14
+ bilingual(`Das Konto, mit dem ${h.name} angemeldet ist, hat kein Guthaben oder Kontingent mehr.`, `The account ${h.name} is signed in with has no credit or quota left.`);
14
15
  /** The failure a client reports, as the person should read it. */
15
16
  export function failureOf(h, detail) {
16
17
  // The client's own words stay in the log: the sentence below sums them up for the person.
@@ -1,6 +1,7 @@
1
1
  import { accessSync, chmodSync, constants } from "node:fs";
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, join } from "node:path";
4
+ import { bilingual } from "@engenty-wizards/shared/run";
4
5
  import { nanoid } from "nanoid";
5
6
  import { ServiceError } from "../services/errors.js";
6
7
  const BUFFER = 200_000;
@@ -36,7 +37,7 @@ async function ptyModule() {
36
37
  return { spawn };
37
38
  }
38
39
  catch {
39
- throw new ServiceError("refused", "Das eingebaute Terminal ist auf diesem Gerät nicht verfügbar. Führe den Befehl in deinem eigenen Terminal aus.");
40
+ throw new ServiceError("refused", bilingual("Das eingebaute Terminal ist auf diesem Gerät nicht verfügbar. Führe den Befehl in deinem eigenen Terminal aus.", "The built-in terminal is not available on this device. Run the command in your own terminal."));
40
41
  }
41
42
  }
42
43
  function emit(session, event) {
@@ -84,7 +85,7 @@ export async function openTerminal(opts) {
84
85
  });
85
86
  }
86
87
  catch (err) {
87
- throw new ServiceError("refused", `Der Befehl konnte nicht starten: ${err.message}`);
88
+ throw new ServiceError("refused", bilingual(`Der Befehl konnte nicht starten: ${err.message}`, `The command could not start: ${err.message}`));
88
89
  }
89
90
  const session = {
90
91
  id,
@@ -146,7 +147,7 @@ export async function openTerminal(opts) {
146
147
  function sessionOf(id) {
147
148
  const session = sessions.get(id);
148
149
  if (!session) {
149
- throw new ServiceError("not_found", "Dieses Terminal gibt es nicht mehr.");
150
+ throw new ServiceError("not_found", bilingual("Dieses Terminal gibt es nicht mehr.", "This terminal is gone."));
150
151
  }
151
152
  return session;
152
153
  }
@@ -1,12 +1,13 @@
1
1
  import { noteText } from "@engenty-wizards/shared/run";
2
2
  /**
3
- * A failure the person reads, given as a note of `RUN_NOTES` where it has one: a run shows it in
4
- * the wizard's language, the message keeps the German line for the log. Free text has no note.
3
+ * A failure the person reads, given as a note of `RUN_NOTES` (or `bilingual` words) where it has
4
+ * one: a run shows it in the wizard's language, a response in the asker's, and the message keeps
5
+ * the English line for the log. Free text has no note.
5
6
  */
6
7
  export class NotedError extends Error {
7
8
  note;
8
9
  constructor(said) {
9
- super(typeof said === "string" ? said : noteText(said, "de"));
10
+ super(typeof said === "string" ? said : noteText(said, "en"));
10
11
  this.note = typeof said === "string" ? null : said;
11
12
  }
12
13
  }
@@ -17,6 +18,10 @@ export class ModelUnavailableError extends NotedError {
17
18
  export function noteOf(err) {
18
19
  return err instanceof NotedError ? err.note : null;
19
20
  }
21
+ /** What a failure says, to pass on: its note where it has one, else its message. */
22
+ export function saidOf(err) {
23
+ return noteOf(err) ?? err?.message ?? String(err);
24
+ }
20
25
  /** A failure in a language: its note where it has one, else its message as it is. */
21
26
  export function errorText(err, lang) {
22
27
  const note = noteOf(err);