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,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
+ }
@@ -1,26 +1,82 @@
1
1
  import { Hono } from "hono";
2
2
  import { withTenant } from "../db/client.js";
3
- import { tenantOfAddress } from "../plugins/public.js";
3
+ import { SHARED, tenantOfAddress } from "../plugins/public.js";
4
4
  import { loadedPlugin, pluginIdsOf } from "../plugins/registry.js";
5
5
  import { tenantStatus } from "../tenants/control.js";
6
6
  import { answerOf } from "./plugins.js";
7
7
  /**
8
8
  * Plugins' public routes (`server.registerPublicRoute`), below `/api/public/plugins/<id>/<ref>`.
9
9
  * Nobody is signed in: the ref names the tenant, and the plugin checks a secret of its own.
10
+ * Shared routes (`server.registerSharedRoute`) are below `/api/public/plugins/<id>/shared`, for
11
+ * every tenant; their handler enters a tenant by the ref the call carries.
10
12
  */
11
13
  const notFound = (c) => c.json({ error: "not found", code: "not_found" }, 404);
12
- async function dispatch(c) {
13
- const id = c.req.param("id") ?? "";
14
- const ref = c.req.param("ref") ?? "";
14
+ /** What a handler threw, as the answer: its status and code, or a 500. */
15
+ function failed(c, id, what, err) {
16
+ const answer = answerOf(err);
17
+ if (answer) {
18
+ return c.json(answer.body, answer.status);
19
+ }
20
+ console.error(`[plugin ${id}] ${what}:`, err);
21
+ return c.json({ error: "The plugin failed.", code: "plugin_failed" }, 500);
22
+ }
23
+ /** The tenant of a ref, when it is active and has the plugin. */
24
+ async function tenantWithPlugin(id, ref) {
15
25
  const tenantId = await tenantOfAddress(id, ref);
16
26
  if (!tenantId || (await tenantStatus(tenantId)) !== "active") {
17
- return notFound(c);
27
+ return null;
18
28
  }
19
- const plugin = (await pluginIdsOf(tenantId)).has(id) ? loadedPlugin(id) : undefined;
29
+ return (await pluginIdsOf(tenantId)).has(id) ? tenantId : null;
30
+ }
31
+ async function shared(c) {
32
+ const id = c.req.param("id") ?? "";
33
+ const plugin = loadedPlugin(id);
20
34
  if (!plugin) {
21
35
  return notFound(c);
22
36
  }
23
37
  const url = new URL(c.req.url);
38
+ const marker = `/plugins/${id}/${SHARED}`;
39
+ const path = url.pathname.slice(url.pathname.indexOf(marker) + marker.length);
40
+ for (const { route, pattern, params } of plugin.sharedRoutes) {
41
+ const match = route.method === c.req.method ? pattern.exec(path) : null;
42
+ if (!match) {
43
+ continue;
44
+ }
45
+ const request = {
46
+ request: c.req.raw,
47
+ path: path || "/",
48
+ params: Object.fromEntries(params.map((name, i) => [name, decodeURIComponent(match[i + 1])])),
49
+ query: url.searchParams,
50
+ async inTenant(ref, work) {
51
+ const tenantId = await tenantWithPlugin(id, ref);
52
+ if (!tenantId) {
53
+ throw Object.assign(new Error("No tenant has this address."), {
54
+ status: 404,
55
+ code: "not_found",
56
+ });
57
+ }
58
+ return withTenant(tenantId, async () => await work());
59
+ },
60
+ };
61
+ try {
62
+ const result = await route.handler(request);
63
+ return result instanceof Response ? result : c.json(result ?? null);
64
+ }
65
+ catch (err) {
66
+ return failed(c, id, `shared ${route.method} ${path}`, err);
67
+ }
68
+ }
69
+ return notFound(c);
70
+ }
71
+ async function dispatch(c) {
72
+ const id = c.req.param("id") ?? "";
73
+ const ref = c.req.param("ref") ?? "";
74
+ const tenantId = await tenantWithPlugin(id, ref);
75
+ const plugin = tenantId ? loadedPlugin(id) : undefined;
76
+ if (!tenantId || !plugin) {
77
+ return notFound(c);
78
+ }
79
+ const url = new URL(c.req.url);
24
80
  const marker = `/plugins/${id}/${ref}`;
25
81
  const path = url.pathname.slice(url.pathname.indexOf(marker) + marker.length);
26
82
  for (const { route, pattern, params } of plugin.publicRoutes) {
@@ -44,16 +100,13 @@ async function dispatch(c) {
44
100
  return result instanceof Response ? result : c.json(result ?? null);
45
101
  }
46
102
  catch (err) {
47
- const answer = answerOf(err);
48
- if (answer) {
49
- return c.json(answer.body, answer.status);
50
- }
51
- console.error(`[plugin ${id}] public ${route.method} ${path}:`, err);
52
- return c.json({ error: "The plugin failed.", code: "plugin_failed" }, 500);
103
+ return failed(c, id, `public ${route.method} ${path}`, err);
53
104
  }
54
105
  }
55
106
  return notFound(c);
56
107
  }
57
108
  export const publicPluginRoutes = new Hono()
109
+ .all(`/:id/${SHARED}`, shared)
110
+ .all(`/:id/${SHARED}/*`, shared)
58
111
  .all("/:id/:ref", dispatch)
59
112
  .all("/:id/:ref/*", dispatch);
@@ -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>
@@ -5,6 +5,7 @@ import { MODEL_CLASSES, TEXT_CLASSES, } from "@engenty-wizards/shared/definition
5
5
  import { CATEGORY_TYPES, whereSchema } from "@engenty-wizards/shared/knowledge";
6
6
  import { PROJECT_FILE_KINDS } from "@engenty-wizards/shared/projects";
7
7
  import { isCreditsRef } from "@engenty-wizards/shared/providers";
8
+ import { bilingual } from "@engenty-wizards/shared/run";
8
9
  import { PACKAGE_EXTENSION, PACKAGE_MAX_BYTES, PACKAGE_MIME, } from "@engenty-wizards/shared/wizard-package";
9
10
  import { generateText } from "ai";
10
11
  import { Hono } from "hono";
@@ -34,6 +35,7 @@ import { installMail, managed, renameTenant, tenantInfo, tenantMembers, userInfo
34
35
  import { seenApps } from "../mcp/seen.js";
35
36
  import { generateImageMedia, generateSpeechMedia, generateVideoMedia } from "../media/generate.js";
36
37
  import { transcribeAudio } from "../media/transcribe.js";
38
+ import { errorText, saidOf } from "../model-errors.js";
37
39
  import { checkKey, classProblem, classWays, embeddingModel, hasTextModel, LOCAL_KEYS, localModelSettings, ModelUnavailableError, modelSettings, saveLocalModels, saveModels, storedKey, textModel, } from "../models.js";
38
40
  import { refreshPro } from "../plugins/pro.js";
39
41
  import { HTML_RESPONSE_CSP } from "../render/guard.js";
@@ -48,7 +50,7 @@ import { categoryContents, listKnowledge, resolvePath, valueContents, } from "..
48
50
  import { writeSummary } from "../services/knowledge-model.js";
49
51
  import { applyWebsite, finishOnboarding, needsOnboarding } from "../services/onboarding.js";
50
52
  import { exportWizard, importWizard } from "../services/package.js";
51
- import { profileSchema, saveProfile, userProfile } from "../services/profile.js";
53
+ import { profileSchema, saveProfile, saveUserLang, userLang, userProfile, } from "../services/profile.js";
52
54
  import { addProjectFile, addProjectFileFromUrl, fileView, orderProjectFiles, projectFile, projectFileContent, projectFiles, reindexProjectFile, removeProjectFile, updateProjectFile, } from "../services/project-files.js";
53
55
  import { documentText, queueIndex, searchKnowledge } from "../services/project-index.js";
54
56
  import { createProject, deleteProject, gradientSchema, listProjects, maskedServers, ownedProject, projectLimitOrNull, projectPatchSchema, updateProject, } from "../services/projects.js";
@@ -63,6 +65,7 @@ import { codeOf } from "../tenants/control.js";
63
65
  import { currentTenant } from "../tenants/tenant.js";
64
66
  import { applyUpdate, updateStatus } from "../update.js";
65
67
  import { byteRange } from "./delivery.js";
68
+ import { inLang, requestLang } from "./lang.js";
66
69
  /** The model sources a test call has worked on: the setup is done for those, not for the others. */
67
70
  async function testedSources() {
68
71
  const tested = await readSetting("setup-done");
@@ -215,6 +218,8 @@ export const studio = new Hono()
215
218
  user: { id: user.id, name: profile.name, email: profile.email, image: user.image ?? null },
216
219
  /** What the person says about themselves; name and e-mail are the account's when managed. */
217
220
  profile,
221
+ /** The language the person picked, wherever they picked it; null until they do. */
222
+ lang: await userLang(user),
218
223
  tenant: {
219
224
  id: user.tenantId,
220
225
  role: user.role,
@@ -305,6 +310,11 @@ export const studio = new Hono()
305
310
  return c.json(await updateStatus());
306
311
  })
307
312
  .put("/profile", async (c) => c.json(await saveProfile(c.get("user"), profileSchema.parse(await c.req.json()))))
313
+ .put("/lang", async (c) => {
314
+ const { lang } = z.object({ lang: z.enum(["de", "en"]) }).parse(await c.req.json());
315
+ await saveUserLang(c.get("user"), lang);
316
+ return c.json({ lang });
317
+ })
308
318
  // --- the studio's assistant -------------------------------------------------
309
319
  // One open chat per person and tenant, on every page; turns stream as SSE events shaped like
310
320
  // AG-UI's (packages/shared/src/assistant.ts).
@@ -323,10 +333,10 @@ export const studio = new Hono()
323
333
  .post("/assistant/speak", async (c) => {
324
334
  const { text } = z.object({ text: z.string().min(1).max(4000) }).parse(await c.req.json());
325
335
  if ((await classWays()).speech.kind === "none") {
326
- throw new ServiceError("refused", "Hier spricht kein Sprachmodell.");
336
+ throw new ServiceError("refused", bilingual("Hier spricht kein Sprachmodell.", "No language model speaks here."));
327
337
  }
328
338
  if (!(await canSpend())) {
329
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
339
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
330
340
  }
331
341
  const voice = await generateSpeechMedia({ text, abortSignal: c.req.raw.signal });
332
342
  return c.body(new Uint8Array(voice.bytes), 200, { "content-type": voice.mime });
@@ -346,10 +356,10 @@ export const studio = new Hono()
346
356
  .post("/assistant/turn", async (c) => {
347
357
  const user = c.get("user");
348
358
  const body = assistantTurnSchema
349
- .extend({ lang: z.enum(["de", "en"]).default("de") })
359
+ .extend({ lang: z.enum(["de", "en"]).default("en") })
350
360
  .parse(await c.req.json());
351
361
  if (!(await canSpend())) {
352
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
362
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
353
363
  }
354
364
  const project = body.projectId ? await ownedProject(user.id, body.projectId) : null;
355
365
  const writable = project ? await projectWritable(project) : false;
@@ -460,7 +470,7 @@ export const studio = new Hono()
460
470
  }
461
471
  await send("error", {
462
472
  message: err instanceof ServiceError || err instanceof ModelUnavailableError
463
- ? err.message
473
+ ? errorText(err, body.lang)
464
474
  : body.lang === "de"
465
475
  ? "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen."
466
476
  : "Something went wrong. Please try again.",
@@ -511,7 +521,7 @@ export const studio = new Hono()
511
521
  const kind = z.enum(PROJECT_FILE_KINDS).parse(c.req.query("kind"));
512
522
  const file = (await c.req.formData()).get("file");
513
523
  if (!(file instanceof File)) {
514
- throw new ServiceError("invalid", "Bitte eine Datei wählen.");
524
+ throw new ServiceError("invalid", bilingual("Bitte eine Datei wählen.", "Please choose a file."));
515
525
  }
516
526
  const row = await addProjectFile(project.id, {
517
527
  kind,
@@ -544,7 +554,7 @@ export const studio = new Hono()
544
554
  await ownedProject(c.get("user").id, c.req.param("id"));
545
555
  const { url } = z.object({ url: z.string().min(1).max(2000) }).parse(await c.req.json());
546
556
  const colors = await scanColors(url).catch(() => {
547
- throw new ServiceError("invalid", "Die Seite ist nicht erreichbar.");
557
+ throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page cannot be reached."));
548
558
  });
549
559
  return c.json({ colors });
550
560
  })
@@ -560,7 +570,7 @@ export const studio = new Hono()
560
570
  })
561
571
  .parse(await c.req.json());
562
572
  if (!(await canSpend())) {
563
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
573
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
564
574
  }
565
575
  try {
566
576
  return c.json({
@@ -569,7 +579,7 @@ export const studio = new Hono()
569
579
  }
570
580
  catch (err) {
571
581
  if (err instanceof ModelUnavailableError) {
572
- throw new ServiceError("refused", err.message);
582
+ throw new ServiceError("refused", saidOf(err));
573
583
  }
574
584
  throw err;
575
585
  }
@@ -578,7 +588,7 @@ export const studio = new Hono()
578
588
  await ownedProject(c.get("user").id, c.req.param("id"));
579
589
  const { url } = z.object({ url: z.string().min(1).max(2000) }).parse(await c.req.json());
580
590
  const fonts = await scanFonts(url).catch(() => {
581
- throw new ServiceError("invalid", "Die Seite ist nicht erreichbar.");
591
+ throw new ServiceError("invalid", bilingual("Die Seite ist nicht erreichbar.", "The page cannot be reached."));
582
592
  });
583
593
  return c.json({ fonts });
584
594
  })
@@ -841,7 +851,7 @@ export const studio = new Hono()
841
851
  format: z.enum(["faq"]).nullish(),
842
852
  })
843
853
  .parse(await c.req.json());
844
- return c.json(await createTable(c.get("user").id, c.req.param("id"), body));
854
+ return c.json(await createTable(c.get("user").id, c.req.param("id"), body, requestLang(c)));
845
855
  })
846
856
  .get("/tables/:id", async (c) => c.json(await getTable(c.get("user").id, c.req.param("id"))))
847
857
  .patch("/tables/:id", async (c) => {
@@ -934,9 +944,9 @@ export const studio = new Hono()
934
944
  .post("/projects/:id/wizards/import", async (c) => {
935
945
  const file = (await c.req.formData()).get("file");
936
946
  if (!(file instanceof File) || !file.size || file.size > PACKAGE_MAX_BYTES) {
937
- throw new ServiceError("invalid", `Bitte ein Wizard-Paket (${PACKAGE_EXTENSION}) bis 30 MB.`);
947
+ throw new ServiceError("invalid", bilingual(`Bitte ein Wizard-Paket (${PACKAGE_EXTENSION}) bis 30 MB.`, `Please a wizard package (${PACKAGE_EXTENSION}) of up to 30 MB.`));
938
948
  }
939
- return c.json(await importWizard(c.get("user").id, c.req.param("id"), new Uint8Array(await file.arrayBuffer()), file.name));
949
+ return c.json(await importWizard(c.get("user").id, c.req.param("id"), new Uint8Array(await file.arrayBuffer()), file.name, requestLang(c)));
940
950
  })
941
951
  .get("/wizards/:id/export", async (c) => {
942
952
  const found = await exportWizard(c.get("user").id, c.req.param("id"), {
@@ -944,7 +954,9 @@ export const studio = new Hono()
944
954
  knowledge: c.req.query("knowledge") === "1",
945
955
  });
946
956
  if (!found) {
947
- return c.json({ error: "Der Wizard ist noch nicht veröffentlicht." }, 404);
957
+ return c.json({
958
+ error: inLang(c, "Der Wizard ist noch nicht veröffentlicht.", "The wizard is not published yet."),
959
+ }, 404);
948
960
  }
949
961
  const { name, zip } = found;
950
962
  return c.body(new Uint8Array(zip), 200, {
@@ -1048,7 +1060,7 @@ export const studio = new Hono()
1048
1060
  // The models the draft needs that this runtime cannot serve, by class, with the steps that call them.
1049
1061
  .get("/wizards/:id/models", async (c) => {
1050
1062
  const w = await ownedWizard(c.get("user").id, c.req.param("id"));
1051
- return c.json({ missing: await missingModels(w.draft) });
1063
+ return c.json({ missing: await missingModels(w.draft, requestLang(c)) });
1052
1064
  })
1053
1065
  // A runtime that runs alone: the wizard's copy in the cloud of the linked account.
1054
1066
  .get("/wizards/:id/cloud", async (c) => {
@@ -1095,13 +1107,13 @@ export const studio = new Hono()
1095
1107
  .parse(await c.req.json());
1096
1108
  await writableWizard(user.id, wizardId);
1097
1109
  if (!(await canSpend())) {
1098
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
1110
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
1099
1111
  }
1100
1112
  return streamSSE(c, async (stream) => {
1101
1113
  const abort = new AbortController();
1102
1114
  stream.onAbort(() => abort.abort());
1103
1115
  try {
1104
- const result = await architectTurn(user.id, wizardId, message, {
1116
+ const result = await architectTurn(user.id, wizardId, message, requestLang(c), {
1105
1117
  signal: abort.signal,
1106
1118
  onText: (delta) => {
1107
1119
  void stream.writeSSE({ event: "text", data: JSON.stringify(delta) });
@@ -1124,8 +1136,8 @@ export const studio = new Hono()
1124
1136
  event: "error",
1125
1137
  data: JSON.stringify({
1126
1138
  message: err instanceof ServiceError
1127
- ? err.message
1128
- : "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen.",
1139
+ ? errorText(err, requestLang(c))
1140
+ : inLang(c, "Da ist etwas schiefgelaufen. Bitte noch einmal versuchen.", "Something went wrong. Please try again."),
1129
1141
  }),
1130
1142
  });
1131
1143
  }
@@ -1135,10 +1147,10 @@ export const studio = new Hono()
1135
1147
  .post("/transcribe", async (c) => {
1136
1148
  const file = (await c.req.formData()).get("file");
1137
1149
  if (!(file instanceof File) || !file.size || file.size > 10_000_000) {
1138
- throw new ServiceError("invalid", "Bitte eine Aufnahme bis 10 MB.");
1150
+ throw new ServiceError("invalid", bilingual("Bitte eine Aufnahme bis 10 MB.", "Please a recording of up to 10 MB."));
1139
1151
  }
1140
1152
  if (!(await canSpend())) {
1141
- throw new ServiceError("no_credits", "Dein Guthaben ist aufgebraucht.");
1153
+ throw new ServiceError("no_credits", bilingual("Dein Guthaben ist aufgebraucht.", "Your credits are used up."));
1142
1154
  }
1143
1155
  try {
1144
1156
  const { text } = await transcribeAudio({
@@ -1149,7 +1161,7 @@ export const studio = new Hono()
1149
1161
  }
1150
1162
  catch (err) {
1151
1163
  if (err instanceof ModelUnavailableError) {
1152
- throw new ServiceError("refused", err.message);
1164
+ throw new ServiceError("refused", saidOf(err));
1153
1165
  }
1154
1166
  throw err;
1155
1167
  }
@@ -1226,7 +1238,7 @@ export const studio = new Hono()
1226
1238
  })
1227
1239
  // --- models: where each class runs, own API keys, the credits behind them -----------------
1228
1240
  // Alone for this machine; on a Manage-App's runtime for the team, whose admins change it.
1229
- .get("/models", async (c) => c.json(await modelSettings()))
1241
+ .get("/models", async (c) => c.json(await modelSettings(requestLang(c))))
1230
1242
  .put("/models", async (c) => {
1231
1243
  if (!mayChangeModels(c.get("user"))) {
1232
1244
  return c.json({ error: "Only an admin of the team changes its models." }, 403);
@@ -1246,13 +1258,13 @@ export const studio = new Hono()
1246
1258
  Object.values(input.bindings ?? {}).some((ref) => ref && !isCreditsRef(ref));
1247
1259
  if (own && info && !feature("ownKeys", info.features)) {
1248
1260
  return c.json({
1249
- error: `Eigene API Keys gibt es nicht im Paket ${info.plan?.name ?? "des Teams"}.`,
1261
+ error: inLang(c, `Eigene API Keys gibt es nicht im Paket ${info.plan?.name ?? "des Teams"}.`, `Own API keys are not part of the ${info.plan?.name ?? "team's"} plan.`),
1250
1262
  code: "plan",
1251
1263
  }, 403);
1252
1264
  }
1253
1265
  }
1254
1266
  await saveModels(input);
1255
- return c.json(await modelSettings());
1267
+ return c.json(await modelSettings(requestLang(c)));
1256
1268
  })
1257
1269
  // Whether a provider takes a key — the one typed, or the one stored. Nothing is generated.
1258
1270
  .post("/models/keys/:provider/check", async (c) => {
@@ -1264,7 +1276,7 @@ export const studio = new Hono()
1264
1276
  if (!value) {
1265
1277
  return c.json({ ok: false, message: "Kein API Key hinterlegt." });
1266
1278
  }
1267
- return c.json(await checkKey(provider, value));
1279
+ return c.json(await checkKey(provider, value, requestLang(c)));
1268
1280
  })
1269
1281
  // One call on a class, as a run makes it: text answers a word, an image, a voice or a short
1270
1282
  // clip comes back to look at, a voice note comes back as text. Paid like any call.
@@ -1273,7 +1285,7 @@ export const studio = new Hono()
1273
1285
  return c.json({ error: "Only an admin of the team tests its models." }, 403);
1274
1286
  }
1275
1287
  const { cls, lang } = z
1276
- .object({ cls: z.enum(MODEL_CLASSES), lang: z.enum(["de", "en"]).default("de") })
1288
+ .object({ cls: z.enum(MODEL_CLASSES), lang: z.enum(["de", "en"]).default("en") })
1277
1289
  .parse(await c.req.json());
1278
1290
  return c.json(await testClass(cls, lang));
1279
1291
  })
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { and, eq } from "drizzle-orm";
3
4
  import { db, schema } from "../db/client.js";
4
5
  import { managed, tenantInfo } from "../manage.js";
@@ -30,7 +31,7 @@ export function currentRole() {
30
31
  return actor.getStore() ?? "owner";
31
32
  }
32
33
  export const isAdminRole = (role) => role === "owner" || role === "admin";
33
- export const MEMBERS_EDIT_ONLY = "Das dürfen nur Admins des Teams. Als Mitglied bearbeitest du die Wizards, die es gibt.";
34
+ export const MEMBERS_EDIT_ONLY = bilingual("Das dürfen nur Admins des Teams. Als Mitglied bearbeitest du die Wizards, die es gibt.", "Only the team's admins may do that. As a member you edit the wizards that are there.");
34
35
  /** Refuses a member: making and deleting is for the owner and the admins of the team. */
35
36
  export async function requireAdmin() {
36
37
  if (syncing() || !managed || isAdminRole(currentRole())) {
@@ -51,8 +52,8 @@ export async function mayBuild() {
51
52
  // An older Manage-App does not say: then it builds. One that cannot be reached: nothing changes.
52
53
  return info ? info.limits.build !== false : false;
53
54
  }
54
- export const READ_ONLY_PROJECT = "Dieses Projekt kommt aus einer lokalen Installation und wird dort geändert.";
55
- export const READ_ONLY_TENANT = "Hier wird nichts gebaut: Auf diesem Server laufen die Wizards, die deine lokale Installation veröffentlicht.";
55
+ export const READ_ONLY_PROJECT = bilingual("Dieses Projekt kommt aus einer lokalen Installation und wird dort geändert.", "This project comes from a local install and is changed there.");
56
+ export const READ_ONLY_TENANT = bilingual("Hier wird nichts gebaut: Auf diesem Server laufen die Wizards, die deine lokale Installation veröffentlicht.", "Nothing is built here: this server runs the wizards your local install publishes.");
56
57
  /** Refuses when the tenant builds nothing here. */
57
58
  export async function requireBuild() {
58
59
  if (syncing() || (await mayBuild())) {
@@ -30,7 +30,9 @@ async function attachedImages(userId, wizardId, message) {
30
30
  return images;
31
31
  }
32
32
  /** One studio chat turn: the architect reads the draft, its workspace and the thread, and changes them with tools. */
33
- export async function architectTurn(userId, wizardId, message, io) {
33
+ export async function architectTurn(userId, wizardId, message,
34
+ /** The language the admin reads: the lines about what it does, and its stock replies. */
35
+ lang, io) {
34
36
  // Asked before the first model call: a wizard that is not changed here costs nothing to refuse.
35
37
  const w = await writableWizard(userId, wizardId);
36
38
  const history = await wizardMessages(w.id);
@@ -51,6 +53,7 @@ export async function architectTurn(userId, wizardId, message, io) {
51
53
  onThought: io.onThought,
52
54
  onBuilding: io.onBuilding,
53
55
  images,
56
+ lang,
54
57
  })),
55
58
  unfinished: false,
56
59
  }
@@ -67,13 +70,21 @@ export async function architectTurn(userId, wizardId, message, io) {
67
70
  onThought: io.onThought,
68
71
  onBuilding: io.onBuilding,
69
72
  images,
73
+ lang,
70
74
  });
75
+ const de = lang === "de";
71
76
  const reply = result.unfinished
72
- ? `${result.reply ? `${result.reply}\n\n` : ""}Ich bin noch nicht ganz fertig geworden. Schreib „weiter“, dann mache ich genau dort weiter.`
77
+ ? `${result.reply ? `${result.reply}\n\n` : ""}${de
78
+ ? "Ich bin noch nicht ganz fertig geworden. Schreib „weiter“, dann mache ich genau dort weiter."
79
+ : "I have not quite finished. Write “continue” and I will carry on right there."}`
73
80
  : result.reply ||
74
81
  (result.changed
75
- ? "Erledigt."
76
- : "Das habe ich nicht verstanden – magst du es anders formulieren?");
82
+ ? de
83
+ ? "Erledigt."
84
+ : "Done."
85
+ : de
86
+ ? "Das habe ich nicht verstanden – magst du es anders formulieren?"
87
+ : "I did not understand that – could you put it differently?");
77
88
  await addMessage(w.id, { role: "assistant", content: reply, changed: result.changed });
78
89
  const after = await ownedWizard(userId, wizardId);
79
90
  return {
@@ -1,4 +1,5 @@
1
1
  import { ASSISTANT_LAYOUTS } from "@engenty-wizards/shared/assistant";
2
+ import { bilingual } from "@engenty-wizards/shared/run";
2
3
  import { generateText } from "ai";
3
4
  import { and, asc, desc, eq, isNotNull, isNull, sql } from "drizzle-orm";
4
5
  import { nanoid } from "nanoid";
@@ -252,7 +253,7 @@ export async function readChat(user, chatId) {
252
253
  where: and(mine(user), eq(schema.assistantChat.id, chatId)),
253
254
  });
254
255
  if (!chat) {
255
- throw new ServiceError("not_found", "Diesen Chat gibt es nicht.");
256
+ throw new ServiceError("not_found", bilingual("Diesen Chat gibt es nicht.", "There is no such chat."));
256
257
  }
257
258
  return {
258
259
  id: chat.id,