wizards 0.4.3 → 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 (129) 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/reference.md +5 -1
  13. package/apps/runtime/dist/docs/dev/plugins/routes.md +35 -1
  14. package/apps/runtime/dist/docs/dev/plugins/server.md +27 -1
  15. package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
  16. package/apps/runtime/dist/documents/parse.js +5 -3
  17. package/apps/runtime/dist/engine/events.js +1 -1
  18. package/apps/runtime/dist/engine/input.js +28 -8
  19. package/apps/runtime/dist/engine/prepare.js +2 -2
  20. package/apps/runtime/dist/engine/requirements.js +12 -3
  21. package/apps/runtime/dist/engine/runner.js +34 -23
  22. package/apps/runtime/dist/engine/steps.js +6 -5
  23. package/apps/runtime/dist/engine/surface.js +2 -1
  24. package/apps/runtime/dist/engine/template.js +1 -1
  25. package/apps/runtime/dist/engine/types.js +2 -1
  26. package/apps/runtime/dist/film/project.js +1 -1
  27. package/apps/runtime/dist/film/skills.js +2 -2
  28. package/apps/runtime/dist/film/sounds.js +1 -1
  29. package/apps/runtime/dist/film/step.js +7 -4
  30. package/apps/runtime/dist/film/tools.js +1 -1
  31. package/apps/runtime/dist/geocode.js +1 -1
  32. package/apps/runtime/dist/harness/apple.js +0 -5
  33. package/apps/runtime/dist/harness/claude.js +2 -1
  34. package/apps/runtime/dist/harness/codex.js +2 -1
  35. package/apps/runtime/dist/harness/model.js +4 -3
  36. package/apps/runtime/dist/harness/terminal.js +4 -3
  37. package/apps/runtime/dist/manage.js +29 -0
  38. package/apps/runtime/dist/model-errors.js +8 -3
  39. package/apps/runtime/dist/models.js +58 -38
  40. package/apps/runtime/dist/plugins/channels.js +77 -0
  41. package/apps/runtime/dist/plugins/loader.js +9 -1
  42. package/apps/runtime/dist/plugins/public.js +16 -2
  43. package/apps/runtime/dist/plugins/registry.js +1 -0
  44. package/apps/runtime/dist/plugins/runs.js +2 -1
  45. package/apps/runtime/dist/routes/api.js +16 -5
  46. package/apps/runtime/dist/routes/lang.js +13 -0
  47. package/apps/runtime/dist/routes/plugin-public.js +65 -12
  48. package/apps/runtime/dist/routes/runs.js +12 -19
  49. package/apps/runtime/dist/routes/studio.js +40 -28
  50. package/apps/runtime/dist/services/access.js +4 -3
  51. package/apps/runtime/dist/services/architect.js +15 -4
  52. package/apps/runtime/dist/services/assistant-chats.js +2 -1
  53. package/apps/runtime/dist/services/brand.js +3 -2
  54. package/apps/runtime/dist/services/errors.js +7 -5
  55. package/apps/runtime/dist/services/knowledge-model.js +3 -2
  56. package/apps/runtime/dist/services/marketplace.js +6 -5
  57. package/apps/runtime/dist/services/package.js +10 -3
  58. package/apps/runtime/dist/services/profile.js +12 -0
  59. package/apps/runtime/dist/services/project-files.js +13 -12
  60. package/apps/runtime/dist/services/projects.js +11 -6
  61. package/apps/runtime/dist/services/runs.js +18 -11
  62. package/apps/runtime/dist/services/shares.js +2 -1
  63. package/apps/runtime/dist/services/site.js +21 -10
  64. package/apps/runtime/dist/services/space-categories.js +7 -6
  65. package/apps/runtime/dist/services/space-data.js +19 -16
  66. package/apps/runtime/dist/services/spaces.js +8 -7
  67. package/apps/runtime/dist/services/wizards.js +41 -20
  68. package/apps/runtime/dist/store/errors.js +2 -1
  69. package/apps/runtime/dist/store/index.js +21 -4
  70. package/apps/runtime/dist/store/shared-lists.js +2 -3
  71. package/apps/runtime/dist/tools/index.js +17 -2
  72. package/apps/runtime/dist/tools/plugin.js +3 -2
  73. package/apps/runtime/dist/widgets/render.js +1 -1
  74. package/apps/runtime/dist/widgets/step.js +2 -1
  75. package/apps/web/dist/assets/{AboutDialog-DWaBKvI6.js → AboutDialog-BCtnYRj2.js} +1 -1
  76. package/apps/web/dist/assets/{ChangelogDialog-BcM1gXPM.js → ChangelogDialog-DBjoIIHL.js} +1 -1
  77. package/apps/web/dist/assets/ChatView-BcyUYN0Q.js +10 -0
  78. package/apps/web/dist/assets/EditorPage-Ce1QD-DK.js +3 -0
  79. package/apps/web/dist/assets/{FlowPreview-S1s1yRaP.js → FlowPreview-BtpAdyt-.js} +1 -1
  80. package/apps/web/dist/assets/{Harness-DILx5Edb.js → Harness-DJJ_yMLY.js} +2 -2
  81. package/apps/web/dist/assets/NewWizardChannels-BrYLMV6F.js +1 -0
  82. package/apps/web/dist/assets/NewWizardPage-827zwhhW.js +7 -0
  83. package/apps/web/dist/assets/{OnboardingPage-xoGrKIJ-.js → OnboardingPage-jC6FidpF.js} +1 -1
  84. package/apps/web/dist/assets/ProjectPage-BYcM1-1H.js +1 -0
  85. package/apps/web/dist/assets/{PublicRunner-Cab2jUy6.js → PublicRunner-BofZl0MO.js} +1 -1
  86. package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-B4nIGkoj.js} +1 -1
  87. package/apps/web/dist/assets/RunDrawer-Dx-BRCkY.js +4 -0
  88. package/apps/web/dist/assets/RunnerView-CDCp61aq.js +1 -0
  89. package/apps/web/dist/assets/SettingsPage-B49nLHwv.js +1 -0
  90. package/apps/web/dist/assets/SetupPage-4tzzHUq0.js +1 -0
  91. package/apps/web/dist/assets/SharePage-Bs4s73h4.js +1 -0
  92. package/apps/web/dist/assets/changelog-D-12LwZB.js +1 -0
  93. package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BueS7qO8.js} +1 -1
  94. package/apps/web/dist/assets/esm-CjGdsKU4.js +7 -0
  95. package/apps/web/dist/assets/index-BRDn-5v_.js +14 -0
  96. package/apps/web/dist/assets/index-mlnAPOdf.css +1 -0
  97. package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-CkXYnQux.js} +1 -1
  98. package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CHkEf-Dz.js} +1 -1
  99. package/apps/web/dist/assets/settings-menu-CrVpArM_.js +1 -0
  100. package/apps/web/dist/assets/ui-DrdqFaQt.js +134 -0
  101. package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-CzkkZqAo.js} +1 -1
  102. package/apps/web/dist/index.html +4 -4
  103. package/apps/web/dist/mcp/flow.html +76 -76
  104. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +98 -2
  105. package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
  106. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
  107. package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
  108. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
  109. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
  110. package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
  111. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
  112. package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
  113. package/package.json +1 -1
  114. package/apps/web/dist/assets/ChatView-D0RRIl15.js +0 -10
  115. package/apps/web/dist/assets/EditorPage-DrJ6e_2q.js +0 -3
  116. package/apps/web/dist/assets/NewWizardChannels-CEILDADp.js +0 -1
  117. package/apps/web/dist/assets/NewWizardPage-DxOtK2E-.js +0 -7
  118. package/apps/web/dist/assets/ProjectPage-6E5z4nZA.js +0 -1
  119. package/apps/web/dist/assets/RunDrawer-B3FNYpgZ.js +0 -4
  120. package/apps/web/dist/assets/RunnerView-D6sGVnRy.js +0 -1
  121. package/apps/web/dist/assets/SettingsPage-DHtIqmOJ.js +0 -1
  122. package/apps/web/dist/assets/SetupPage-Cn9iQbJx.js +0 -1
  123. package/apps/web/dist/assets/SharePage-CMr72PeU.js +0 -1
  124. package/apps/web/dist/assets/changelog-DfmWTlav.js +0 -1
  125. package/apps/web/dist/assets/esm-BPfTI3lL.js +0 -7
  126. package/apps/web/dist/assets/index-B-D8UvL9.js +0 -14
  127. package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
  128. package/apps/web/dist/assets/settings-menu-BJm0CwOj.js +0 -1
  129. package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
@@ -1,4 +1,5 @@
1
1
  import { GRADIENT_TYPES, gradientCss, mainLogo, } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { generateText, Output } from "ai";
3
4
  import { and, asc, eq } from "drizzle-orm";
4
5
  import { z } from "zod";
@@ -65,14 +66,14 @@ export async function describeGradient(brand, text, current) {
65
66
  answer = result.output;
66
67
  }
67
68
  catch {
68
- throw new ServiceError("invalid", "Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.");
69
+ throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
69
70
  }
70
71
  const stops = answer.stops
71
72
  .filter((s) => HEX.test(s.color))
72
73
  .slice(0, 12)
73
74
  .map((s) => ({ color: s.color.toLowerCase(), at: Math.min(100, Math.max(0, s.at)) }));
74
75
  if (stops.length < 2) {
75
- throw new ServiceError("invalid", "Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.");
76
+ throw new ServiceError("invalid", bilingual("Daraus ließ sich kein Verlauf machen. Beschreib ihn bitte anders.", "No gradient could be made from that. Please describe it differently."));
76
77
  }
77
78
  return {
78
79
  name: answer.name.trim().slice(0, 80),
@@ -1,3 +1,4 @@
1
+ import { errorText, NotedError } from "../model-errors.js";
1
2
  const STATUS = {
2
3
  not_found: 404,
3
4
  invalid: 400,
@@ -8,19 +9,20 @@ const STATUS = {
8
9
  read_only: 403,
9
10
  forbidden: 403,
10
11
  };
11
- export class ServiceError extends Error {
12
+ export class ServiceError extends NotedError {
12
13
  code;
13
14
  data;
14
- constructor(code, message, data = {}) {
15
- super(message);
15
+ constructor(code, said, data = {}) {
16
+ super(said);
16
17
  this.code = code;
17
18
  this.data = data;
18
19
  }
19
20
  get status() {
20
21
  return STATUS[this.code];
21
22
  }
22
- toJSON() {
23
- return { error: this.message, code: this.code, ...this.data };
23
+ /** The answer, its words in the asker's language; English where none is known. */
24
+ toJSON(lang = "en") {
25
+ return { error: errorText(this, lang), code: this.code, ...this.data };
24
26
  }
25
27
  }
26
28
  export const notFound = () => new ServiceError("not_found", "not found");
@@ -1,4 +1,5 @@
1
1
  import { dayOf, slugOf } from "@engenty-wizards/shared/knowledge";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { columnIdOf } from "@engenty-wizards/shared/space-data";
3
4
  import { generateText, Output } from "ai";
4
5
  import { and, eq, inArray, isNull } from "drizzle-orm";
@@ -222,7 +223,7 @@ function titleOf(markdown, name) {
222
223
  function reviewOf(parsed, markdown) {
223
224
  const illegible = (markdown.match(/\[unleserlich\]/gi) ?? []).length;
224
225
  if (parsed.via === "vision" && (illegible >= 3 || markdown.replace(/\s/g, "").length < 200)) {
225
- return "Schwer lesbar: bitte mit dem Original vergleichen.";
226
+ return "Hard to read: please compare with the original.";
226
227
  }
227
228
  return null;
228
229
  }
@@ -503,7 +504,7 @@ export async function writeSummary(valueId) {
503
504
  where: eq(schema.spaceCategoryValue.id, valueId),
504
505
  });
505
506
  if (!value) {
506
- throw new ServiceError("not_found", "Diesen Wert gibt es nicht.");
507
+ throw new ServiceError("not_found", bilingual("Diesen Wert gibt es nicht.", "There is no such value."));
507
508
  }
508
509
  const category = await categoryRow(value.categoryId);
509
510
  const { items, total } = await itemsOfValue(value.id);
@@ -4,6 +4,7 @@ import { DEFINITION_VERSION, readable, wizardSchema, } from "@engenty-wizards/sh
4
4
  import { capabilitiesOf, costTierOf, effortOf, formatsOf, MARKETPLACE_LANGS, pluginsOf, } from "@engenty-wizards/shared/marketplace";
5
5
  import { wizardOutline } from "@engenty-wizards/shared/marketplace-entry";
6
6
  import { searchEntries } from "@engenty-wizards/shared/marketplace-search";
7
+ import { bilingual } from "@engenty-wizards/shared/run";
7
8
  import { and, eq, inArray, notInArray } from "drizzle-orm";
8
9
  import { formulaEstimate } from "../credits/estimate.js";
9
10
  import { control, controlDb, db, schema, withTenant } from "../db/client.js";
@@ -25,7 +26,7 @@ import { ServiceError } from "./errors.js";
25
26
  * listed first and never fetched. They are known as `<plugin id>.<starter id>`; a marketplace
26
27
  * entry's id has no dot, so the two never meet.
27
28
  */
28
- export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "de";
29
+ export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "en";
29
30
  /** A search waits this long for the marketplace; then it searches what is kept here. */
30
31
  const TIMEOUT_MS = 5000;
31
32
  /** One call to the marketplace's API; null without an answer, or with an error. */
@@ -114,7 +115,7 @@ function pluginSummary({ id, plugin, starter }) {
114
115
  .slice(0, 16),
115
116
  updatedAt: (changed?.mtime ?? new Date()).toISOString(),
116
117
  starter: true,
117
- language: starter.language ?? "de",
118
+ language: starter.language ?? "en",
118
119
  title: starter.title,
119
120
  pitch: starter.description,
120
121
  // Found by the plugin's name as well: "Termine" finds the appointment plugin's starters.
@@ -390,7 +391,7 @@ export async function marketplaceWizard(id, lang) {
390
391
  revision: null,
391
392
  plugin: plugin.source.id,
392
393
  title: starter.title,
393
- language: starter.language ?? "de",
394
+ language: starter.language ?? "en",
394
395
  definition: structuredClone(starter.definition),
395
396
  files: Object.fromEntries(Object.entries(starter.files ?? {}).map(([path, data]) => [
396
397
  path,
@@ -403,13 +404,13 @@ export async function marketplaceWizard(id, lang) {
403
404
  return null;
404
405
  }
405
406
  if (!readable(found.definition)) {
406
- throw new ServiceError("refused", "Diese Vorlage braucht eine neuere Version der App. Bitte aktualisiere die App.");
407
+ throw new ServiceError("refused", bilingual("Diese Vorlage braucht eine neuere Version der App. Bitte aktualisiere die App.", "This template needs a newer version of the app. Please update the app."));
407
408
  }
408
409
  // A wizard whose plugins are not here would stop at its first step that uses them.
409
410
  const have = await pluginIdsOf(currentTenant());
410
411
  const needs = pluginsOf(wizardSchema.parse(found.definition)).filter((p) => !have.has(p));
411
412
  if (needs.length) {
412
- throw new ServiceError("refused", `Diese Vorlage braucht ${needs.length === 1 ? "das Plugin" : "die Plugins"} ${needs.join(", ")}: mit Pro auf engenty.ai dabei.`);
413
+ throw new ServiceError("refused", bilingual(`Diese Vorlage braucht ${needs.length === 1 ? "das Plugin" : "die Plugins"} ${needs.join(", ")}: mit Pro auf engenty.ai dabei.`, `This template needs the ${needs.length === 1 ? "plugin" : "plugins"} ${needs.join(", ")}: included with Pro on engenty.ai.`));
413
414
  }
414
415
  return {
415
416
  id: found.id,
@@ -136,16 +136,23 @@ async function missing(userId, projectId, draft, manifest) {
136
136
  ];
137
137
  }
138
138
  /** Makes a new wizard of a package in the project: the draft and its workspace, not published. */
139
- export async function importWizard(userId, projectId, data, name) {
139
+ export async function importWizard(userId, projectId, data, name,
140
+ /** The language the admin reads the import's note in. */
141
+ lang = "en") {
140
142
  await requireAdmin();
141
143
  const { manifest, files } = readPackage(data);
142
144
  // The shape is checked before anything is written; a wrong one leaves no wizard behind.
143
145
  const { draft } = parseDraft(manifest.definition, files.map((f) => f.path));
144
146
  const lacks = await missing(userId, projectId, draft, manifest);
147
+ const de = lang === "de";
145
148
  const note = [
146
- `Importiert${name ? ` aus „${name}“` : ""}: ${draft.steps.length} Schritte, ${files.length} ${files.length === 1 ? "Datei" : "Dateien"}.`,
149
+ de
150
+ ? `Importiert${name ? ` aus „${name}“` : ""}: ${draft.steps.length} Schritte, ${files.length} ${files.length === 1 ? "Datei" : "Dateien"}.`
151
+ : `Imported${name ? ` from “${name}”` : ""}: ${draft.steps.length} steps, ${files.length} ${files.length === 1 ? "file" : "files"}.`,
147
152
  lacks.length
148
- ? `Diesem Projekt fehlt noch: ${lacks.join(", ")} – in den Einstellungen unter „Connectors“ hinzufügen.`
153
+ ? de
154
+ ? `Diesem Projekt fehlt noch: ${lacks.join(", ")} – in den Einstellungen unter „Connectors“ hinzufügen.`
155
+ : `This project still lacks: ${lacks.join(", ")} – add it under “Connectors” in the settings.`
149
156
  : "",
150
157
  ]
151
158
  .filter(Boolean)
@@ -27,6 +27,18 @@ export async function userProfile(user) {
27
27
  phone: stored.phone ?? "",
28
28
  };
29
29
  }
30
+ const langKey = (user) => `lang:${user.tenantId}:${user.id}`;
31
+ /**
32
+ * The language the person picked in the studio, kept with them rather than one browser: a new
33
+ * browser or device opens in it. Null until they pick one; the browser's language stands then.
34
+ */
35
+ export async function userLang(user) {
36
+ const lang = await readSetting(langKey(user));
37
+ return lang === "de" || lang === "en" ? lang : null;
38
+ }
39
+ export async function saveUserLang(user, lang) {
40
+ await writeSetting(langKey(user), lang);
41
+ }
30
42
  export async function saveProfile(user, profile) {
31
43
  // Name and e-mail an account gives stay the person's own underneath: they come back unlinked.
32
44
  const stored = (await readSetting(key(user))) ?? {};
@@ -1,4 +1,5 @@
1
1
  import { PROJECT_LIMITS, projectFileLimit, } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { generateText } from "ai";
3
4
  import { and, asc, count, eq, max } from "drizzle-orm";
4
5
  import { db, schema } from "../db/client.js";
@@ -27,7 +28,7 @@ export function fileView(row) {
27
28
  description: row.description,
28
29
  source: row.source,
29
30
  status: stale ? "failed" : row.status,
30
- error: stale ? "Unterbrochen. Bitte neu einlesen." : row.error,
31
+ error: stale ? "Interrupted. Please read it in again." : row.error,
31
32
  indexed: row.indexed,
32
33
  pages: row.pages,
33
34
  chars: row.chars,
@@ -78,14 +79,14 @@ export async function addProjectFile(projectId, input) {
78
79
  const mime = documentMime(input.name, input.mime) || "application/octet-stream";
79
80
  if (!accepts(input.kind, mime)) {
80
81
  throw new ServiceError("invalid", input.kind === "logo"
81
- ? "Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP)."
82
+ ? bilingual("Ein Logo ist ein Bild (PNG, SVG, JPEG, WebP).", "A logo is an image (PNG, SVG, JPEG, WebP).")
82
83
  : input.kind === "pattern"
83
- ? "Ein Muster ist ein Bild (SVG, PNG, JPEG, WebP)."
84
- : "Hierher gehören Bilder, Videos, Audio, PDFs, Schriften, Lottie und 3D-Modelle. Texte zum Nachschlagen bitte zu den Dokumenten.");
84
+ ? bilingual("Ein Muster ist ein Bild (SVG, PNG, JPEG, WebP).", "A pattern is an image (SVG, PNG, JPEG, WebP).")
85
+ : bilingual("Hierher gehören Bilder, Videos, Audio, PDFs, Schriften, Lottie und 3D-Modelle. Texte zum Nachschlagen bitte zu den Dokumenten.", "Images, videos, audio, PDFs, fonts, Lottie and 3D models belong here. Texts to look things up in go to the documents."));
85
86
  }
86
87
  const limit = projectFileLimit(input.kind, mime);
87
88
  if (!input.data.byteLength || input.data.byteLength > limit) {
88
- throw new ServiceError("invalid", `Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`);
89
+ throw new ServiceError("invalid", bilingual(`Bitte eine Datei bis ${Math.round(limit / 1_000_000)} MB wählen.`, `Please choose a file of up to ${Math.round(limit / 1_000_000)} MB.`));
89
90
  }
90
91
  const scope = and(eq(schema.projectFile.projectId, projectId), eq(schema.projectFile.kind, input.kind));
91
92
  const [{ n, last }] = await db
@@ -93,7 +94,7 @@ export async function addProjectFile(projectId, input) {
93
94
  .from(schema.projectFile)
94
95
  .where(scope);
95
96
  if (n >= PROJECT_LIMITS.files[input.kind]) {
96
- throw new ServiceError("refused", `Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`);
97
+ throw new ServiceError("refused", bilingual(`Höchstens ${PROJECT_LIMITS.files[input.kind]} Dateien.`, `At most ${PROJECT_LIMITS.files[input.kind]} files.`));
97
98
  }
98
99
  const name = input.name.slice(0, 120) || "datei";
99
100
  const ref = await saveAsset({
@@ -149,21 +150,21 @@ export async function addProjectFileFromUrl(projectId, input) {
149
150
  headers: { "user-agent": "Mozilla/5.0 (compatible; engenty-wizards/0.1)" },
150
151
  signal: AbortSignal.any([AbortSignal.timeout(60_000), ...(signal ? [signal] : [])]),
151
152
  }).catch(() => {
152
- throw new ServiceError("invalid", "Die Adresse ist nicht erreichbar.");
153
+ throw new ServiceError("invalid", bilingual("Die Adresse ist nicht erreichbar.", "The address cannot be reached."));
153
154
  });
154
155
  if (!res.ok) {
155
- throw new ServiceError("invalid", `Die Adresse antwortete ${res.status}.`);
156
+ throw new ServiceError("invalid", bilingual(`Die Adresse antwortete ${res.status}.`, `The address answered ${res.status}.`));
156
157
  }
157
158
  const mime = (res.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
158
159
  if (mime === "text/html" && kind !== "document") {
159
160
  await res.body?.cancel();
160
- throw new ServiceError("invalid", "Unter der Adresse steht eine Webseite, kein Bild.", {
161
+ throw new ServiceError("invalid", bilingual("Unter der Adresse steht eine Webseite, kein Bild.", "The address holds a web page, not an image."), {
161
162
  reason: "page",
162
163
  url: res.url,
163
164
  });
164
165
  }
165
166
  if (Number(res.headers.get("content-length") ?? 0) > projectFileLimit(kind, mime)) {
166
- throw new ServiceError("invalid", "Die Datei ist zu groß.");
167
+ throw new ServiceError("invalid", bilingual("Die Datei ist zu groß.", "The file is too large."));
167
168
  }
168
169
  const data = new Uint8Array(await res.arrayBuffer());
169
170
  const name = decodeURIComponent(new URL(res.url).pathname.split("/").pop() || kind);
@@ -297,7 +298,7 @@ async function prepare(row) {
297
298
  status: "failed",
298
299
  error: err instanceof UnreadableDocument
299
300
  ? err.message
300
- : "Das Lesen hat nicht geklappt. Bitte noch einmal versuchen.",
301
+ : "Reading did not work. Please try again.",
301
302
  });
302
303
  }
303
304
  });
@@ -306,7 +307,7 @@ export async function reindexProjectFile(projectId, fileId) {
306
307
  await requireWritableProject(projectId);
307
308
  const row = await projectFile(projectId, fileId);
308
309
  if (row.kind !== "document") {
309
- throw new ServiceError("invalid", "Nur Dokumente stehen im Index.");
310
+ throw new ServiceError("invalid", bilingual("Nur Dokumente stehen im Index.", "Only documents are in the index."));
310
311
  }
311
312
  const [pending] = await db
312
313
  .update(schema.projectFile)
@@ -1,4 +1,5 @@
1
1
  import { COLOR_SECTION_KINDS, cleanAssetSections, cleanLogoSections, FACT_TYPES, FONT_USES, FONT_WEIGHTS, GRADIENT_TYPES, orderColors, PROJECT_LIMITS, RULE_MARKS, } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, count, eq, isNull } from "drizzle-orm";
3
4
  import { nanoid } from "nanoid";
4
5
  import { z } from "zod";
@@ -154,9 +155,11 @@ async function ensureProject() {
154
155
  const any = await db.query.project.findFirst({ where: madeHere() });
155
156
  if (!any) {
156
157
  const id = nanoid(12);
157
- await db
158
- .insert(schema.project)
159
- .values({ id, tenantId: currentTenant(), name: "Meine Wizards" });
158
+ await db.insert(schema.project).values({
159
+ id,
160
+ tenantId: currentTenant(),
161
+ name: env.manage.url ? "Meine Wizards" : "Local Space",
162
+ });
160
163
  spaceChanged("space.created", id);
161
164
  }
162
165
  }
@@ -222,7 +225,9 @@ export async function createProject(_userId, name) {
222
225
  .where(eq(schema.project.tenantId, currentTenant()));
223
226
  const limit = await projectLimit();
224
227
  if (n >= limit) {
225
- throw new ServiceError("refused", limit === 1 ? "Hier gibt es ein Projekt." : `Höchstens ${limit} Projekte.`);
228
+ throw new ServiceError("refused", limit === 1
229
+ ? bilingual("Hier gibt es ein Projekt.", "There is one project here.")
230
+ : bilingual(`Höchstens ${limit} Projekte.`, `At most ${limit} projects.`));
226
231
  }
227
232
  const id = nanoid(12);
228
233
  await db
@@ -246,7 +251,7 @@ export async function updateProject(userId, projectId, patch) {
246
251
  return { ...s, headers };
247
252
  });
248
253
  if (patch.facts && new Set(patch.facts.map((f) => f.key)).size !== patch.facts.length) {
249
- throw new ServiceError("invalid", "Zwei Einträge haben denselben Schlüssel.");
254
+ throw new ServiceError("invalid", bilingual("Zwei Einträge haben denselben Schlüssel.", "Two entries have the same key."));
250
255
  }
251
256
  await db
252
257
  .update(schema.project)
@@ -360,7 +365,7 @@ export async function deleteProject(userId, projectId) {
360
365
  await requireAdmin();
361
366
  const all = await db.query.project.findMany({ where: madeHere() });
362
367
  if (all.length <= 1) {
363
- throw new ServiceError("refused", "Das letzte Projekt bleibt.");
368
+ throw new ServiceError("refused", bilingual("Das letzte Projekt bleibt.", "The last project stays."));
364
369
  }
365
370
  await removeProject(projectId);
366
371
  }
@@ -1,11 +1,13 @@
1
1
  import { FIELD_WAYS, formatsFor, isDecidedField, shownFields, slotText, wizardLang, } from "@engenty-wizards/shared/definition";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, count, desc, eq, gte, inArray, max, or, sql } from "drizzle-orm";
3
4
  import { canSpend, MICROS_PER_CREDIT } from "../credits/credits.js";
4
5
  import { db, schema } from "../db/client.js";
5
6
  import { answerAsk } from "../engine/asks.js";
6
7
  import { emitEvent, recentEvents, watchRun } from "../engine/events.js";
8
+ import { fieldErrorsIn } from "../engine/input.js";
7
9
  import { cancel, createRun, goBack, NoCreditsError, pageContext, RunConflict, RunInputError, retry, reviewStep, submitPage, } from "../engine/runner.js";
8
- import { ModelUnavailableError } from "../model-errors.js";
10
+ import { ModelUnavailableError, saidOf } from "../model-errors.js";
9
11
  import { signedUrl } from "../secrets/signing.js";
10
12
  import { currentTenant } from "../tenants/tenant.js";
11
13
  import { notFound, ServiceError } from "./errors.js";
@@ -16,7 +18,7 @@ export async function startTestRun(userId, wizardId, autopilot) {
16
18
  const w = await ownedWizard(userId, wizardId);
17
19
  const { definition, files } = await requireClean(w);
18
20
  if (!(await canSpend())) {
19
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
21
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
20
22
  }
21
23
  const runId = await createRun({
22
24
  wizardId: w.id,
@@ -27,7 +29,7 @@ export async function startTestRun(userId, wizardId, autopilot) {
27
29
  userId,
28
30
  unattended: Boolean(autopilot),
29
31
  }).catch((err) => {
30
- throw err instanceof ModelUnavailableError ? new ServiceError("refused", err.message) : err;
32
+ throw err instanceof ModelUnavailableError ? new ServiceError("refused", saidOf(err)) : err;
31
33
  });
32
34
  if (autopilot) {
33
35
  fly(runId, autopilot.answers, autopilot.acceptReviews);
@@ -78,9 +80,14 @@ function fly(runId, answers, acceptReviews) {
78
80
  } while (again);
79
81
  }
80
82
  catch (err) {
83
+ const answers = (lang) => err instanceof RunInputError
84
+ ? fieldErrorsIn(err.errors, lang)
85
+ .map((e) => `${e.field} – ${e.message}`)
86
+ .join("; ")
87
+ : "";
81
88
  const message = err instanceof RunInputError
82
- ? `Antworten fehlen oder passen nicht: ${err.errors.map((e) => `${e.field} – ${e.message}`).join("; ")}`
83
- : err.message;
89
+ ? bilingual(`Antworten fehlen oder passen nicht: ${answers("de")}`, `Answers are missing or do not fit: ${answers("en")}`)
90
+ : saidOf(err);
84
91
  await emitEvent(runId, null, "error", message).catch(() => undefined);
85
92
  land();
86
93
  }
@@ -347,10 +354,10 @@ export const testRunReport = (userId, runId, waitSeconds = 0) => runReport(userI
347
354
  /** A command the run refused, as a tool error the client can act on. */
348
355
  export function refusedInput(err) {
349
356
  if (err instanceof RunInputError) {
350
- throw new ServiceError("invalid", err.message, { fields: err.errors });
357
+ throw new ServiceError("invalid", saidOf(err), { fields: fieldErrorsIn(err.errors, "en") });
351
358
  }
352
359
  if (err instanceof RunConflict) {
353
- throw new ServiceError("refused", err.message);
360
+ throw new ServiceError("refused", saidOf(err));
354
361
  }
355
362
  throw err;
356
363
  }
@@ -370,7 +377,7 @@ export async function startRun(userId, wizardId, answers = {}) {
370
377
  throw notFound();
371
378
  }
372
379
  if (!(await canSpend())) {
373
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
380
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
374
381
  }
375
382
  const runId = await createRun({
376
383
  wizardId: w.id,
@@ -381,10 +388,10 @@ export async function startRun(userId, wizardId, answers = {}) {
381
388
  userId,
382
389
  }).catch((err) => {
383
390
  if (err instanceof ModelUnavailableError) {
384
- throw new ServiceError("refused", err.message);
391
+ throw new ServiceError("refused", saidOf(err));
385
392
  }
386
393
  if (err instanceof NoCreditsError) {
387
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
394
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
388
395
  }
389
396
  throw err;
390
397
  });
@@ -400,7 +407,7 @@ export async function startRun(userId, wizardId, answers = {}) {
400
407
  }
401
408
  catch (err) {
402
409
  if (err instanceof RunInputError) {
403
- return { runId, refused: err.errors };
410
+ return { runId, refused: fieldErrorsIn(err.errors, "en") };
404
411
  }
405
412
  throw err;
406
413
  }
@@ -1,3 +1,4 @@
1
+ import { bilingual } from "@engenty-wizards/shared/run";
1
2
  import { and, eq, isNotNull, lt } from "drizzle-orm";
2
3
  import { nanoid } from "nanoid";
3
4
  import { db, schema } from "../db/client.js";
@@ -12,7 +13,7 @@ export const shareUrlFor = (token) => `${env.publicUrl}/s/${token}`;
12
13
  /** Shares a finished run. Idempotent: the same run keeps the same link. */
13
14
  export async function shareRun(run) {
14
15
  if (run.status !== "done") {
15
- throw new ServiceError("refused", "Nur fertige Ergebnisse lassen sich teilen.");
16
+ throw new ServiceError("refused", bilingual("Nur fertige Ergebnisse lassen sich teilen.", "Only finished results can be shared."));
16
17
  }
17
18
  let token = run.shareToken;
18
19
  if (!token) {
@@ -1,8 +1,10 @@
1
1
  import { factKey, PROJECT_LIMITS } from "@engenty-wizards/shared/projects";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { nanoid } from "nanoid";
3
4
  import { z } from "zod";
4
5
  import { runStudioAssistant } from "../agents/studio-assistant.js";
5
6
  import { canSpend } from "../credits/credits.js";
7
+ import { errorText } from "../model-errors.js";
6
8
  import { currentTenant } from "../tenants/tenant.js";
7
9
  import { projectWritable, requireWritable } from "./access.js";
8
10
  import { apiSpace, apiSpaceDetails } from "./api.js";
@@ -55,6 +57,8 @@ export const siteSchema = z.object({
55
57
  /** Let the space assistant fill in about and facts; false writes only what the site states. */
56
58
  assist: z.boolean().default(true),
57
59
  });
60
+ /** The language the site's people read: German for a German site, else English. */
61
+ const siteLang = (input) => input.language?.toLowerCase().startsWith("de") ? "de" : "en";
58
62
  const jobs = new Map();
59
63
  const KEEP_MS = 60 * 60 * 1000;
60
64
  function forget() {
@@ -146,16 +150,16 @@ function assistMessage(input) {
146
150
  .slice(0, 6)
147
151
  .map((p) => `## ${p.title}${p.url ? ` (${p.url})` : ""}${p.role ? ` [${p.role}]` : ""}\n\n${p.markdown.slice(0, 6000)}`);
148
152
  return [
149
- `Das ist die WordPress-Website ${input.url}. Fülle den Space aus dem, was sie über sich sagt: Titel, eine Beschreibung (about), Fakten (Adresse, Telefon, E-Mail, UID, Firmenbuch, Öffnungszeiten, wer sie betreibt) und die Farben, falls noch keine da sind. Ihre Seiten stehen schon in Wissen; schreib sie nicht ab.`,
150
- "Die Angaben unten kommen direkt aus WordPress. Lies die Website mit read_website nur, wenn etwas Wichtiges fehlt. Erfinde nichts.",
151
- `Name: ${input.name || "—"}\nUntertitel: ${input.tagline || "—"}\nSprache: ${input.language ?? "—"}`,
153
+ `This is the WordPress website ${input.url}. Fill the space from what it says about itself: a title, a description (about), facts (address, phone, email, VAT id, company register, opening hours, who runs it) and the colours, if there are none yet. Its pages are already in Knowledge; do not copy them. Write in the website's own language.`,
154
+ "The details below come straight from WordPress. Read the website with read_website only when something important is missing. Invent nothing.",
155
+ `Name: ${input.name || "—"}\nTagline: ${input.tagline || "—"}\nLanguage: ${input.language ?? "—"}`,
152
156
  input.facts.length
153
- ? `Angaben der Website:\n${input.facts.map((f) => `- ${f.label}: ${f.value}`).join("\n")}`
157
+ ? `What the website states:\n${input.facts.map((f) => `- ${f.label}: ${f.value}`).join("\n")}`
154
158
  : "",
155
159
  input.colors.length
156
- ? `Farben des Themes: ${input.colors.map((c) => `${c.name} ${c.value}`).join(", ")}`
160
+ ? `The theme's colours: ${input.colors.map((c) => `${c.name} ${c.value}`).join(", ")}`
157
161
  : "",
158
- pages.length ? `Seiten:\n\n${pages.join("\n\n")}` : "",
162
+ pages.length ? `Pages:\n\n${pages.join("\n\n")}` : "",
159
163
  ]
160
164
  .filter(Boolean)
161
165
  .join("\n\n")
@@ -163,7 +167,10 @@ function assistMessage(input) {
163
167
  }
164
168
  async function work(job, projectId, input) {
165
169
  try {
166
- job.activity = "Übernimmt die Angaben der Website …";
170
+ job.activity =
171
+ siteLang(input) === "de"
172
+ ? "Übernimmt die Angaben der Website …"
173
+ : "Taking over what the website says …";
167
174
  await fillFromSite(job.userId, projectId, input);
168
175
  job.activity = "Schreibt die Seiten in Wissen …";
169
176
  job.pages = await writePages(projectId, input);
@@ -181,7 +188,7 @@ async function work(job, projectId, input) {
181
188
  message: assistMessage(input),
182
189
  page: null,
183
190
  browserTools: [],
184
- lang: input.language?.toLowerCase().startsWith("en") ? "en" : "de",
191
+ lang: siteLang(input),
185
192
  onActivity: (label) => {
186
193
  job.activity = label;
187
194
  },
@@ -201,7 +208,11 @@ async function work(job, projectId, input) {
201
208
  catch (err) {
202
209
  job.status = "failed";
203
210
  job.error =
204
- err instanceof ServiceError ? err.message : "Die Website ließ sich nicht übernehmen.";
211
+ err instanceof ServiceError
212
+ ? errorText(err, siteLang(input))
213
+ : siteLang(input) === "de"
214
+ ? "Die Website ließ sich nicht übernehmen."
215
+ : "The website could not be taken over.";
205
216
  console.error("[site]", err);
206
217
  }
207
218
  }
@@ -256,7 +267,7 @@ function siteJobView(job) {
256
267
  export function siteJob(jobId) {
257
268
  const job = jobs.get(jobId);
258
269
  if (!job || job.tenantId !== currentTenant()) {
259
- throw new ServiceError("not_found", "Diesen Vorgang gibt es nicht (mehr).");
270
+ throw new ServiceError("not_found", bilingual("Diesen Vorgang gibt es nicht (mehr).", "There is no such job (any more)."));
260
271
  }
261
272
  return siteJobView(job);
262
273
  }
@@ -1,4 +1,5 @@
1
1
  import { CATEGORY_TYPES, dayOf, } from "@engenty-wizards/shared/knowledge";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, asc, eq, inArray, isNotNull, or, sql } from "drizzle-orm";
3
4
  import { nanoid } from "nanoid";
4
5
  import { db, schema } from "../db/client.js";
@@ -272,10 +273,10 @@ const MAX_CATEGORIES = 60;
272
273
  function checkDraft(draft) {
273
274
  const name = draft.name.replace(/\s+/g, " ").trim().slice(0, 80);
274
275
  if (!name) {
275
- throw new ServiceError("invalid", "Eine Kategorie braucht einen Namen.");
276
+ throw new ServiceError("invalid", bilingual("Eine Kategorie braucht einen Namen.", "A category needs a name."));
276
277
  }
277
278
  if (!CATEGORY_TYPES.includes(draft.type)) {
278
- throw new ServiceError("invalid", `Unbekannter Typ „${draft.type}“.`);
279
+ throw new ServiceError("invalid", bilingual(`Unbekannter Typ „${draft.type}“.`, `Unknown type “${draft.type}”.`));
279
280
  }
280
281
  return name;
281
282
  }
@@ -329,7 +330,7 @@ export async function putCategory(projectId, draft, opts = {}) {
329
330
  return known;
330
331
  }
331
332
  if (rows.length >= MAX_CATEGORIES) {
332
- throw new ServiceError("refused", `Ein Space hat höchstens ${MAX_CATEGORIES} Kategorien.`);
333
+ throw new ServiceError("refused", bilingual(`Ein Space hat höchstens ${MAX_CATEGORIES} Kategorien.`, `A space has at most ${MAX_CATEGORIES} categories.`));
333
334
  }
334
335
  const id = nanoid(12);
335
336
  const [row] = await db
@@ -408,7 +409,7 @@ export async function updateCategory(categoryId, patch) {
408
409
  if (name !== row.name) {
409
410
  const clash = byName(await categoryRows(row.projectId), name);
410
411
  if (clash && clash.id !== row.id) {
411
- throw new ServiceError("invalid", `Die Kategorie „${name}“ gibt es schon.`);
412
+ throw new ServiceError("invalid", bilingual(`Die Kategorie „${name}“ gibt es schon.`, `The category “${name}” already exists.`));
412
413
  }
413
414
  }
414
415
  await db
@@ -458,7 +459,7 @@ export async function addCategoryValue(categoryId, value) {
458
459
  const category = await categoryRow(categoryId);
459
460
  await requireWritableProject(category.projectId);
460
461
  if (category.type !== "choice") {
461
- throw new ServiceError("invalid", "Nur eine Auswahl hat Werte.");
462
+ throw new ServiceError("invalid", bilingual("Nur eine Auswahl hat Werte.", "Only a choice has values."));
462
463
  }
463
464
  const ids = await addValues(category.id, [value]);
464
465
  return { id: [...ids.values()].at(-1) };
@@ -488,7 +489,7 @@ export async function mergeCategoryValues(fromId, intoId) {
488
489
  const from = await valueRow(fromId);
489
490
  const into = await valueRow(intoId);
490
491
  if (from.category.id !== into.category.id || fromId === intoId) {
491
- throw new ServiceError("invalid", "Nur Werte einer Kategorie lassen sich zusammenführen.");
492
+ throw new ServiceError("invalid", bilingual("Nur Werte einer Kategorie lassen sich zusammenführen.", "Only values of one category can be merged."));
492
493
  }
493
494
  await requireWritableProject(from.category.projectId);
494
495
  const t = schema.spaceItemCategory;