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
@@ -8,15 +8,16 @@ import { createOpenAI } from "@ai-sdk/openai";
8
8
  import { createReplicate } from "@ai-sdk/replicate";
9
9
  import { MODEL_CLASSES, TEXT_CLASSES, } from "@engenty-wizards/shared/definition";
10
10
  import { creditsModelOf, isCreditsRef, PROVIDER_IDS, providerInfo, providerOfRef, } from "@engenty-wizards/shared/providers";
11
+ import { bilingual, saidText } from "@engenty-wizards/shared/run";
11
12
  import { generateSpeech, generateText, } from "ai";
12
13
  import { accountToken, linkedAccount } from "./auth/account.js";
13
14
  import { DeviceModel, deviceThinks } from "./engine/device.js";
14
15
  import { env } from "./env.js";
15
- import { appleModel, appleStatus, appleTranscriptionModel, appleUnavailable, appleUnavailableNote, } from "./harness/apple.js";
16
+ import { appleModel, appleStatus, appleTranscriptionModel, appleUnavailableNote, } from "./harness/apple.js";
16
17
  import { detectHarness, HarnessModel, harness, isHarnessVendor, notInstalled, signedOut, } from "./harness/index.js";
17
18
  import { managed } from "./manage.js";
18
19
  import { hasFfmpeg } from "./media/ffmpeg.js";
19
- import { ModelUnavailableError } from "./model-errors.js";
20
+ import { ModelUnavailableError, saidOf } from "./model-errors.js";
20
21
  import { seal, unseal } from "./secrets/crypto.js";
21
22
  import { vaultDelete, vaultGet, vaultSet } from "./secrets/vault.js";
22
23
  import { readSetting, writeSetting } from "./settings.js";
@@ -231,27 +232,29 @@ function ownRef(cfg, cls) {
231
232
  // The defaults name gateway models; without a gateway key nothing reaches them.
232
233
  return reachable(cfg, env.models[cls]) ? env.models[cls] : "";
233
234
  }
234
- const NOTHING = {
235
- classifier: "Für Text ist kein Modell eingerichtet.",
236
- standard: "Für Text ist kein Modell eingerichtet.",
237
- high: "Für Text ist kein Modell eingerichtet.",
238
- highest: "Für Text ist kein Modell eingerichtet.",
239
- image: "Für Bilder ist kein Modell eingerichtet.",
240
- video: "Für Videos ist kein Modell eingerichtet.",
241
- speech: "Für Sprachausgabe ist kein Modell eingerichtet.",
242
- audio: "Für Sprachnotizen ist kein Modell eingerichtet.",
235
+ /** What a class makes, to say that nothing makes it. */
236
+ const MAKES = {
237
+ classifier: { de: "Text", en: "text" },
238
+ standard: { de: "Text", en: "text" },
239
+ high: { de: "Text", en: "text" },
240
+ highest: { de: "Text", en: "text" },
241
+ image: { de: "Bilder", en: "images" },
242
+ video: { de: "Videos", en: "videos" },
243
+ speech: { de: "Sprachausgabe", en: "speech" },
244
+ audio: { de: "Sprachnotizen", en: "voice notes" },
243
245
  };
244
246
  /** Why a class has no way: the provider picked for it lacks its key, or nothing is set up. */
245
247
  function missingText(cfg, cls) {
246
248
  const bound = cfg.settings.bindings[cls];
247
249
  if (isCreditsRef(bound)) {
248
- return "Dafür ist das Guthaben gewählt, aber kein Konto angemeldet.";
250
+ return bilingual("Dafür ist das Guthaben gewählt, aber kein Konto angemeldet.", "The credits are picked for this, but no account is signed in.");
249
251
  }
252
+ const makes = MAKES[cls];
250
253
  const provider = bound ? providerInfo(providerOfRef(bound) ?? "") : null;
251
254
  if (provider) {
252
- return `${NOTHING[cls].replace(/ ist kein Modell eingerichtet\.$/, "")} ist ${provider.name} gewählt, aber kein API Key hinterlegt.`;
255
+ return bilingual(`Für ${makes.de} ist ${provider.name} gewählt, aber kein API Key hinterlegt.`, `${provider.name} is picked for ${makes.en}, but no API key is set.`);
253
256
  }
254
- return NOTHING[cls];
257
+ return bilingual(`Für ${makes.de} ist kein Modell eingerichtet.`, `No model is set up for ${makes.en}.`);
255
258
  }
256
259
  /**
257
260
  * The way to the credits. On a Manage-App's runtime always; alone, through the linked account —
@@ -271,7 +274,7 @@ async function creditAccess(cfg, chosen) {
271
274
  const token = await accountToken();
272
275
  if (!token) {
273
276
  if (chosen) {
274
- throw new ModelUnavailableError("Das Konto ist nicht mehr angemeldet. Bitte in den Einstellungen neu anmelden.");
277
+ throw new ModelUnavailableError(bilingual("Das Konto ist nicht mehr angemeldet. Bitte in den Einstellungen neu anmelden.", "The account is no longer signed in. Please sign in again in the settings."));
275
278
  }
276
279
  return null;
277
280
  }
@@ -298,7 +301,7 @@ async function routeOf(cls) {
298
301
  function need(cfg, name) {
299
302
  const key = cfg.keys[name];
300
303
  if (!key) {
301
- throw new ModelUnavailableError(`Für ${providerInfo(name)?.name ?? name} ist kein API Key hinterlegt.`);
304
+ throw new ModelUnavailableError(bilingual(`Für ${providerInfo(name)?.name ?? name} ist kein API Key hinterlegt.`, `No API key is set for ${providerInfo(name)?.name ?? name}.`));
302
305
  }
303
306
  return key;
304
307
  }
@@ -409,7 +412,7 @@ async function ownLanguageModel(cfg, ref, meta) {
409
412
  };
410
413
  }
411
414
  }
412
- throw new ModelUnavailableError(`Für das Modell "${ref}" ist kein Zugang eingerichtet. Bitte in den Einstellungen einen API Key hinterlegen oder ein Konto anmelden.`);
415
+ throw new ModelUnavailableError(bilingual(`Für das Modell "${ref}" ist kein Zugang eingerichtet. Bitte in den Einstellungen einen API Key hinterlegen oder ein Konto anmelden.`, `There is no access set up for the model "${ref}". Please add an API key or sign in to an account in the settings.`));
413
416
  }
414
417
  /**
415
418
  * The language model a text class runs on. A run watched from a device that offers the class
@@ -489,7 +492,7 @@ export async function imageModel(meta = {}) {
489
492
  return own(createOpenAI({ apiKey: cfg.keys.openai }).image(aliasOf(id)));
490
493
  }
491
494
  }
492
- throw new ModelUnavailableError("Für Bilder ist kein Modell eingerichtet.");
495
+ throw new ModelUnavailableError(bilingual("Für Bilder ist kein Modell eingerichtet.", "No model is set up for images."));
493
496
  }
494
497
  export async function videoModel(meta = {}) {
495
498
  const route = await routeOf("video");
@@ -518,7 +521,7 @@ export async function videoModel(meta = {}) {
518
521
  return own(createGateway({ apiKey: cfg.keys.gateway }).videoModel(id), true);
519
522
  }
520
523
  }
521
- throw new ModelUnavailableError("Für Videos ist kein Modell eingerichtet.");
524
+ throw new ModelUnavailableError(bilingual("Für Videos ist kein Modell eingerichtet.", "No model is set up for videos."));
522
525
  }
523
526
  /** The model that reads a text aloud. */
524
527
  export async function speechModel(meta = {}) {
@@ -526,7 +529,7 @@ export async function speechModel(meta = {}) {
526
529
  if (route.kind === "credits") {
527
530
  const resolved = await viaCredits(route, "speech", meta, (c, id) => c.speechModel(id));
528
531
  if (!resolved.ref) {
529
- throw new ModelUnavailableError("Für Sprachausgabe ist kein Modell eingerichtet.");
532
+ throw new ModelUnavailableError(bilingual("Für Sprachausgabe ist kein Modell eingerichtet.", "No model is set up for speech."));
530
533
  }
531
534
  return resolved;
532
535
  }
@@ -557,7 +560,7 @@ export async function speechModel(meta = {}) {
557
560
  return own(createOpenAI({ apiKey: cfg.keys.openai }).speech(aliasOf(id)));
558
561
  }
559
562
  }
560
- throw new ModelUnavailableError("Für Sprachausgabe ist kein Modell eingerichtet.");
563
+ throw new ModelUnavailableError(bilingual("Für Sprachausgabe ist kein Modell eingerichtet.", "No model is set up for speech."));
561
564
  }
562
565
  /**
563
566
  * What turns a voice note into text: a transcription model (ElevenLabs Scribe, Whisper), or a
@@ -708,7 +711,7 @@ export function freeTierRefusal(err) {
708
711
  }
709
712
  /** What the person reads when Vercel's free tier leaves a model out, and how to fix it. */
710
713
  export function freeTierText(model) {
711
- return `Dein AI Gateway Key ist bei Vercel im kostenlosen Tarif, und der schließt ${model ? `„${model}“` : "dieses Modell"} aus. Abhilfe: bei Vercel Guthaben aufladen (${GATEWAY_TOP_UP}) oder unter Einstellungen → Modelle dafür einen anderen Weg wählen.`;
714
+ return bilingual(`Dein AI Gateway Key ist bei Vercel im kostenlosen Tarif, und der schließt ${model ? `„${model}“` : "dieses Modell"} aus. Abhilfe: bei Vercel Guthaben aufladen (${GATEWAY_TOP_UP}) oder unter Einstellungen → Modelle dafür einen anderen Weg wählen.`, `Your AI Gateway key is on Vercel's free tier, which leaves out ${model ? `“${model}”` : "this model"}. To fix it: top up at Vercel (${GATEWAY_TOP_UP}) or pick another way for it under Settings → Models.`);
712
715
  }
713
716
  /** What a key was answered for a model: refusals are asked again soon, as a top-up lifts them. */
714
717
  const gatewayProbes = new Map();
@@ -762,7 +765,7 @@ const CONVERSATION_MODEL = env.conversationModel || "gpt-realtime";
762
765
  export async function conversationAccess() {
763
766
  const cfg = await config();
764
767
  if (!cfg.keys.openai) {
765
- throw new ModelUnavailableError("Für Sprachgespräche ist kein OpenAI API Key hinterlegt (Einstellungen → Modelle).");
768
+ throw new ModelUnavailableError(bilingual("Für Sprachgespräche ist kein OpenAI API Key hinterlegt (Einstellungen → Modelle).", "No OpenAI API key is set for voice conversations (Settings → Models)."));
766
769
  }
767
770
  return { apiKey: cfg.keys.openai, model: CONVERSATION_MODEL };
768
771
  }
@@ -799,11 +802,11 @@ export async function classProblem(cls) {
799
802
  const named = creditsModelOf((await config()).settings.bindings[cls]);
800
803
  if (named) {
801
804
  return catalog?.models && !catalog.models.some((m) => m.id === named)
802
- ? "Dieses Modell gibt es beim Konto nicht."
805
+ ? bilingual("Dieses Modell gibt es beim Konto nicht.", "The account has no such model.")
803
806
  : null;
804
807
  }
805
808
  return catalog?.classes && !catalog.classes[cls]
806
- ? "Dafür ist beim Konto kein Modell eingerichtet."
809
+ ? bilingual("Dafür ist beim Konto kein Modell eingerichtet.", "The account has no model set up for this.")
807
810
  : null;
808
811
  }
809
812
  const client = harness(resolved.vendor);
@@ -814,7 +817,7 @@ export async function classProblem(cls) {
814
817
  }
815
818
  catch (err) {
816
819
  if (err instanceof ModelUnavailableError) {
817
- return err.message;
820
+ return saidOf(err);
818
821
  }
819
822
  throw err;
820
823
  }
@@ -830,7 +833,7 @@ export async function filmClient(cls) {
830
833
  const client = managed ? null : harness(cfg.settings.source);
831
834
  if (!client || !FILM_CLIENTS.has(client.id)) {
832
835
  return {
833
- problem: "Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter „Modelle“ als KI-Abo ein.",
836
+ problem: bilingual("Filme macht Claude Code auf diesem Computer mit deinem Abo. Richte es unter „Modelle“ als KI-Abo ein.", "Films are made by Claude Code on this computer with your subscription. Set it up as an AI subscription under “Models”."),
834
837
  };
835
838
  }
836
839
  const status = await detectHarness(client.id);
@@ -885,7 +888,7 @@ export async function classWays() {
885
888
  }
886
889
  let problem = null;
887
890
  const access = await creditAccess(cfg, creditsChosen(cfg, cls)).catch((err) => {
888
- problem = err.message;
891
+ problem = saidOf(err);
889
892
  return null;
890
893
  });
891
894
  ways[cls] = access
@@ -910,25 +913,32 @@ export async function classWays() {
910
913
  async function appleProblem(cls) {
911
914
  const status = await appleStatus();
912
915
  if (cls !== "audio") {
913
- return status.available ? null : appleUnavailable(status);
916
+ return status.available ? null : appleUnavailableNote(status);
914
917
  }
915
918
  if (!status.supported || status.reason === "missing") {
916
- return appleUnavailable(status);
919
+ return appleUnavailableNote(status);
917
920
  }
918
921
  return (await hasFfmpeg())
919
922
  ? null
920
- : "Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf; Safari und die App brauchen es nicht. ffmpeg installieren, z. B. mit „brew install ffmpeg“.";
923
+ : bilingual("Aufnahmen aus Chrome oder Firefox (WebM, Ogg) schreibt dieser Mac erst mit ffmpeg auf; Safari und die App brauchen es nicht. ffmpeg installieren, z. B. mit „brew install ffmpeg“.", "Recordings from Chrome or Firefox (WebM, Ogg) need ffmpeg before this Mac can write them down; Safari and the app do not. Install ffmpeg, e.g. with “brew install ffmpeg”.");
921
924
  }
922
925
  /** At start the ways are asked once, so a model the key cannot reach shows before a run meets it. */
923
926
  export async function checkModelsAtStart() {
924
927
  for (const [cls, way] of Object.entries(await classWays())) {
925
928
  if (way.kind !== "none" && way.problem) {
926
- console.warn(`models: ${cls}: ${way.problem}`);
929
+ console.warn(`models: ${cls}: ${saidText(way.problem, "en")}`);
927
930
  }
928
931
  }
929
932
  }
933
+ /** The ways with their problems in a language, for whoever reads them. */
934
+ export function waysIn(ways, lang) {
935
+ return Object.fromEntries(Object.entries(ways).map(([cls, way]) => [
936
+ cls,
937
+ { ...way, problem: way.problem && saidText(way.problem, lang) },
938
+ ]));
939
+ }
930
940
  /** What the settings page shows: the choices, which keys are there (never the keys), and the ways. */
931
- export async function modelSettings() {
941
+ export async function modelSettings(lang = "en") {
932
942
  const cfg = await config();
933
943
  return {
934
944
  source: cfg.settings.source,
@@ -936,7 +946,7 @@ export async function modelSettings() {
936
946
  ollamaUrl: cfg.settings.ollamaUrl ?? env.ollamaUrl,
937
947
  creditFallback: cfg.settings.creditFallback !== false,
938
948
  keys: Object.fromEntries(LOCAL_KEYS.map((k) => [k, Boolean(cfg.keys[k])])),
939
- ways: await classWays(),
949
+ ways: waysIn(await classWays(), lang),
940
950
  /** Apple Intelligence on this Mac: shown where the machine can run it, picked once it answers. */
941
951
  apple: managed ? null : await appleStatus(),
942
952
  /** What a class costs on the credits, where they are at hand. */
@@ -947,7 +957,8 @@ export async function modelSettings() {
947
957
  }
948
958
  // --- Checking a key ------------------------------------------------------------------
949
959
  /** A cheap authenticated call: whether the provider takes the key. Nothing is generated. */
950
- export async function checkKey(name, key) {
960
+ export async function checkKey(name, key, lang = "en") {
961
+ const de = lang === "de";
951
962
  const requests = {
952
963
  openai: ["https://api.openai.com/v1/models", { authorization: `Bearer ${key}` }],
953
964
  anthropic: [
@@ -980,12 +991,21 @@ export async function checkKey(name, key) {
980
991
  return {
981
992
  ok: false,
982
993
  message: res.status === 401
983
- ? "Der Anbieter kennt diesen Key nicht."
984
- : `Der Anbieter antwortet mit ${res.status}.`,
994
+ ? de
995
+ ? "Der Anbieter kennt diesen Key nicht."
996
+ : "The provider does not know this key."
997
+ : de
998
+ ? `Der Anbieter antwortet mit ${res.status}.`
999
+ : `The provider answers with ${res.status}.`,
985
1000
  };
986
1001
  }
987
1002
  catch {
988
- return { ok: false, message: "Der Anbieter ist gerade nicht erreichbar." };
1003
+ return {
1004
+ ok: false,
1005
+ message: de
1006
+ ? "Der Anbieter ist gerade nicht erreichbar."
1007
+ : "The provider cannot be reached right now.",
1008
+ };
989
1009
  }
990
1010
  }
991
1011
  let catalog = new Map();
@@ -5,6 +5,7 @@ import { and, asc, eq, inArray, isNotNull } from "drizzle-orm";
5
5
  import { db, schema, withTenant } from "../db/client.js";
6
6
  import { answerAsk } from "../engine/asks.js";
7
7
  import { watchRun } from "../engine/events.js";
8
+ import { fieldErrorsIn } from "../engine/input.js";
8
9
  import { cancel, createRun, goBack, NoCreditsError, RunInputError, retry, reviewStep, submitPage, } from "../engine/runner.js";
9
10
  import { saveAsset } from "../files/storage.js";
10
11
  import { wizardUnavailable } from "../limits.js";
@@ -81,7 +82,7 @@ export function runsApiFor(pluginId) {
81
82
  if (!(err instanceof RunInputError)) {
82
83
  throw err;
83
84
  }
84
- refused = err.errors;
85
+ refused = fieldErrorsIn(err.errors, "en");
85
86
  }
86
87
  }
87
88
  return { runId, ticket: runTicket(runId), refused };
@@ -1,3 +1,4 @@
1
+ import { bilingual } from "@engenty-wizards/shared/run";
1
2
  import { PACKAGE_MIME } from "@engenty-wizards/shared/wizard-package";
2
3
  import { Hono } from "hono";
3
4
  import { bodyLimit } from "hono/body-limit";
@@ -19,6 +20,7 @@ import { siteJob, siteSchema, startSiteJob } from "../services/site.js";
19
20
  import { checkOnServer, checkSchema, listSyncedSpaces, patchSyncedWizard, removeSyncedSpace, removeSyncedWizard, rotateSyncedLink, syncDataSchema, syncSettingsSchema, syncSpaceData, syncWizard, syncWizardSchema, } from "../services/spaces.js";
20
21
  import { createWizard, ownedWizard, publishWizard, writeDraft } from "../services/wizards.js";
21
22
  import { codeOf, tenantStatus } from "../tenants/control.js";
23
+ import { inLang } from "./lang.js";
22
24
  import { dispatchAs } from "./plugins.js";
23
25
  const importSchema = z.object({
24
26
  /** An earlier import's wizard: it is updated instead of a new one being made. */
@@ -66,7 +68,10 @@ async function caller(c, scopes) {
66
68
  function withinSyncLimit(c, who) {
67
69
  return allowed(`sync:${who.tenantId}`, 240, HOUR)
68
70
  ? null
69
- : c.json({ error: "Zu viele Übertragungen. Bitte später noch einmal.", code: "rate_limited" }, 429);
71
+ : c.json({
72
+ error: inLang(c, "Zu viele Übertragungen. Bitte später noch einmal.", "Too many transfers. Please try again later."),
73
+ code: "rate_limited",
74
+ }, 429);
70
75
  }
71
76
  /**
72
77
  * A plugin's routes for a client with a key or token (the WordPress plugin's Kontakte, Termine):
@@ -82,7 +87,7 @@ async function pluginCall(c) {
82
87
  if (!(await pluginIdsOf(who.tenantId)).has(id)) {
83
88
  return loadedPlugin(id) || PRO_PLUGINS.includes(id)
84
89
  ? c.json({
85
- error: "Dieses Plugin gehört nicht zu deinem Paket. Mit Pro ist es dabei.",
90
+ error: inLang(c, "Dieses Plugin gehört nicht zu deinem Paket. Mit Pro ist es dabei.", "This plugin is not part of your plan. It comes with Pro."),
86
91
  code: "plugin",
87
92
  plugin: id,
88
93
  }, 403)
@@ -171,7 +176,10 @@ export const apiRoutes = new Hono()
171
176
  knowledge: c.req.query("knowledge") === "1",
172
177
  });
173
178
  if (!found) {
174
- return c.json({ error: "Der Wizard ist noch nicht veröffentlicht.", code: "not_found" }, 404);
179
+ return c.json({
180
+ error: inLang(c, "Der Wizard ist noch nicht veröffentlicht.", "The wizard is not published yet."),
181
+ code: "not_found",
182
+ }, 404);
175
183
  }
176
184
  return new Response(new Uint8Array(found.zip), {
177
185
  headers: {
@@ -278,7 +286,10 @@ export const apiRoutes = new Hono()
278
286
  return who;
279
287
  }
280
288
  if (!allowed(`site:${who.tenantId}`, 20, HOUR)) {
281
- return c.json({ error: "Zu oft hintereinander. Bitte später noch einmal.", code: "rate_limited" }, 429);
289
+ return c.json({
290
+ error: inLang(c, "Zu oft hintereinander. Bitte später noch einmal.", "Too often in a row. Please try again later."),
291
+ code: "rate_limited",
292
+ }, 429);
282
293
  }
283
294
  const body = siteSchema.parse(await c.req.json());
284
295
  return inTenantAs(who, async () => c.json(await startSiteJob(who, body)));
@@ -410,7 +421,7 @@ export const apiRoutes = new Hono()
410
421
  return c.json({ wizardId, revision: written.revision, issues: written.issues });
411
422
  }
412
423
  if (written.issues.length) {
413
- throw new ServiceError("has_issues", "Der Wizard hat noch Fehler.", {
424
+ throw new ServiceError("has_issues", bilingual("Der Wizard hat noch Fehler.", "The wizard still has errors."), {
414
425
  issues: written.issues,
415
426
  wizardId,
416
427
  });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The language a visitor's request asks for: what the runner around it speaks, the browser's
3
+ * first language (apps/web: `navigator.language`) — German for "de…", else English; English
4
+ * without one, as the runner starts.
5
+ */
6
+ export function requestLang(c) {
7
+ const asked = c.req.header("accept-language")?.trim().toLowerCase();
8
+ return asked?.startsWith("de") ? "de" : "en";
9
+ }
10
+ /** A line the person reads, in the language their request asks for. */
11
+ export function inLang(c, de, en) {
12
+ return requestLang(c) === "de" ? de : en;
13
+ }
@@ -10,12 +10,13 @@ import { nanoid } from "nanoid";
10
10
  import { z } from "zod";
11
11
  import { principalOf } from "../auth/index.js";
12
12
  import { MARK_FOCUSED_PERSONAL } from "../browser/snapshot.js";
13
- import { ConnectError, connectionViews, connectWithCredentials, disconnect, finishOAuth, oauthStateRun, startOAuth, } from "../connectors/index.js";
13
+ import { ConnectError, connectionViews, connectWithCredentials, disconnect, finishOAuth, oauthStateLang, oauthStateRun, startOAuth, } from "../connectors/index.js";
14
14
  import { finishPluginOAuth, pluginStateTenant } from "../connectors/plugin-connections.js";
15
15
  import { db, schema, withTenant } from "../db/client.js";
16
16
  import { answerAsk, pendingAsk } from "../engine/asks.js";
17
17
  import { answerDevice, offerDevice, openDeviceCalls } from "../engine/device.js";
18
18
  import { emitEvent, signalChanged, watchRun } from "../engine/events.js";
19
+ import { fieldErrorsIn } from "../engine/input.js";
19
20
  import { fromOwner } from "../engine/owner.js";
20
21
  import { pushAvailable, setPushDevice } from "../engine/push.js";
21
22
  import { liveResources } from "../engine/resources.js";
@@ -38,6 +39,7 @@ import { isIconFile, wizardAvatarSvg, wizardIcon } from "../services/wizard-icon
38
39
  import { clearStore, deleteRows, listRows, listSecrets, readStoreFile, StoreError, saveRows, scopeOf, storeFiles, updateRow, } from "../store/index.js";
39
40
  import { tenantOfLink, tenantOfRun, tenantStatus, tokenOfCode, visitorOverLimit, } from "../tenants/control.js";
40
41
  import { byteRange, uploadLimit, wizardManifest } from "./delivery.js";
42
+ import { requestLang } from "./lang.js";
41
43
  const VISITOR_COOKIE = "wz_vid";
42
44
  /** Kept files a review shows inline. */
43
45
  const INLINE_TYPES = new Set(["application/pdf", "image/png", "image/jpeg", "image/webp"]);
@@ -53,15 +55,6 @@ export function clientIp(c) {
53
55
  return null;
54
56
  }
55
57
  }
56
- /**
57
- * The language a visitor's request asks for: what the runner around it speaks, the browser's
58
- * first language (apps/web: `navigator.language`) — German for "de…", else English; German
59
- * without one, as the runner starts.
60
- */
61
- function requestLang(c) {
62
- const asked = c.req.header("accept-language")?.trim().toLowerCase();
63
- return !asked || asked.startsWith("de") ? "de" : "en";
64
- }
65
58
  /** A refusal the visitor reads, in the language their request asks for. */
66
59
  function refuse(c, note, status) {
67
60
  return c.json({ error: noteText(note, requestLang(c)) }, status);
@@ -140,7 +133,7 @@ async function viewOf(run) {
140
133
  }
141
134
  function commandError(c, err) {
142
135
  if (err instanceof RunInputError) {
143
- return c.json({ error: errorText(err, requestLang(c)), fields: err.errors }, 422);
136
+ return c.json({ error: errorText(err, requestLang(c)), fields: fieldErrorsIn(err.errors, requestLang(c)) }, 422);
144
137
  }
145
138
  if (err instanceof RunConflict) {
146
139
  return c.json({ error: errorText(err, requestLang(c)) }, 409);
@@ -633,7 +626,7 @@ export const runRoutes = new Hono()
633
626
  .object({
634
627
  lat: z.number().min(-90).max(90),
635
628
  lng: z.number().min(-180).max(180),
636
- language: z.enum(["de", "en"]).default("de"),
629
+ language: z.enum(["de", "en"]).default("en"),
637
630
  })
638
631
  .parse(await c.req.json());
639
632
  return c.json({ label: await reverseGeocode(lat, lng, language) });
@@ -841,12 +834,12 @@ export const runRoutes = new Hono()
841
834
  return c.json({ error: "not found" }, 404);
842
835
  }
843
836
  try {
844
- const url = await startOAuth(scopeOf(run), await projectIdOf(run), run.id, connection, c.req.param("connectorId"));
837
+ const url = await startOAuth(scopeOf(run), await projectIdOf(run), run.id, connection, c.req.param("connectorId"), requestLang(c));
845
838
  return c.json({ url });
846
839
  }
847
840
  catch (err) {
848
841
  if (err instanceof ConnectError) {
849
- return c.json({ error: err.message }, 422);
842
+ return c.json({ error: errorText(err, requestLang(c)) }, 422);
850
843
  }
851
844
  throw err;
852
845
  }
@@ -867,7 +860,7 @@ export const runRoutes = new Hono()
867
860
  }
868
861
  catch (err) {
869
862
  if (err instanceof ConnectError) {
870
- return c.json({ error: err.message }, 422);
863
+ return c.json({ error: errorText(err, requestLang(c)) }, 422);
871
864
  }
872
865
  throw err;
873
866
  }
@@ -965,7 +958,7 @@ export const runRoutes = new Hono()
965
958
  return c.json({
966
959
  lists,
967
960
  files: await storeFiles(scope),
968
- connections: (await connectionViews(run.definition.connections ?? [], scope, await projectIdOf(run))).filter((x) => x.account),
961
+ connections: (await connectionViews(run.definition.connections ?? [], scope, await projectIdOf(run), requestLang(c))).filter((x) => x.account),
969
962
  keepsSignIns: secrets.some((s) => s.slot === "browser"),
970
963
  });
971
964
  })
@@ -1036,7 +1029,7 @@ export const runRoutes = new Hono()
1036
1029
  }
1037
1030
  catch (err) {
1038
1031
  if (err instanceof ServiceError) {
1039
- return c.json(err.toJSON(), err.status);
1032
+ return c.json(err.toJSON(requestLang(c)), err.status);
1040
1033
  }
1041
1034
  throw err;
1042
1035
  }
@@ -1051,7 +1044,7 @@ export const runRoutes = new Hono()
1051
1044
  });
1052
1045
  function listError(c, err) {
1053
1046
  if (err instanceof TableColumnValueError || err instanceof StoreError) {
1054
- return c.json({ error: err.message }, 422);
1047
+ return c.json({ error: errorText(err, requestLang(c)) }, 422);
1055
1048
  }
1056
1049
  throw err;
1057
1050
  }
@@ -1098,7 +1091,7 @@ export const connectCallback = new Hono().get("/callback", async (c) => {
1098
1091
  said = { code: "connectFailed" };
1099
1092
  }
1100
1093
  }
1101
- const message = noteText(said, requestLang(c));
1094
+ const message = noteText(said, (state && oauthStateLang(state)) || requestLang(c));
1102
1095
  return c.html(`<!doctype html><meta charset="utf-8"><title>engenty wizards</title>
1103
1096
  <body style="font:16px system-ui;display:grid;place-items:center;height:100vh;margin:0;color:#333">
1104
1097
  <p>${message}</p>