wizards 0.4.4 → 0.4.6

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 +3 -2
  13. package/apps/runtime/dist/docs/dev/plugins/runners.md +38 -2
  14. package/apps/runtime/dist/docs/dev/plugins/server.md +3 -2
  15. package/apps/runtime/dist/docs/dev/plugins/starters.md +1 -1
  16. package/apps/runtime/dist/docs/dev/plugins/studio.md +2 -1
  17. package/apps/runtime/dist/documents/parse.js +5 -3
  18. package/apps/runtime/dist/engine/events.js +1 -1
  19. package/apps/runtime/dist/engine/input.js +28 -8
  20. package/apps/runtime/dist/engine/prepare.js +2 -2
  21. package/apps/runtime/dist/engine/requirements.js +12 -3
  22. package/apps/runtime/dist/engine/runner.js +34 -23
  23. package/apps/runtime/dist/engine/steps.js +6 -5
  24. package/apps/runtime/dist/engine/surface.js +2 -1
  25. package/apps/runtime/dist/engine/template.js +1 -1
  26. package/apps/runtime/dist/engine/types.js +2 -1
  27. package/apps/runtime/dist/film/project.js +1 -1
  28. package/apps/runtime/dist/film/skills.js +2 -2
  29. package/apps/runtime/dist/film/sounds.js +1 -1
  30. package/apps/runtime/dist/film/step.js +7 -4
  31. package/apps/runtime/dist/film/tools.js +1 -1
  32. package/apps/runtime/dist/geocode.js +1 -1
  33. package/apps/runtime/dist/harness/apple.js +0 -5
  34. package/apps/runtime/dist/harness/claude.js +2 -1
  35. package/apps/runtime/dist/harness/codex.js +2 -1
  36. package/apps/runtime/dist/harness/model.js +4 -3
  37. package/apps/runtime/dist/harness/terminal.js +4 -3
  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 +1 -0
  41. package/apps/runtime/dist/plugins/runs.js +2 -1
  42. package/apps/runtime/dist/routes/api.js +16 -5
  43. package/apps/runtime/dist/routes/lang.js +13 -0
  44. package/apps/runtime/dist/routes/runs.js +12 -19
  45. package/apps/runtime/dist/routes/studio.js +40 -28
  46. package/apps/runtime/dist/services/access.js +4 -3
  47. package/apps/runtime/dist/services/architect.js +15 -4
  48. package/apps/runtime/dist/services/assistant-chats.js +2 -1
  49. package/apps/runtime/dist/services/brand.js +3 -2
  50. package/apps/runtime/dist/services/errors.js +7 -5
  51. package/apps/runtime/dist/services/knowledge-model.js +3 -2
  52. package/apps/runtime/dist/services/marketplace.js +6 -5
  53. package/apps/runtime/dist/services/package.js +10 -3
  54. package/apps/runtime/dist/services/profile.js +12 -0
  55. package/apps/runtime/dist/services/project-files.js +13 -12
  56. package/apps/runtime/dist/services/projects.js +11 -6
  57. package/apps/runtime/dist/services/runners.js +17 -2
  58. package/apps/runtime/dist/services/runs.js +18 -11
  59. package/apps/runtime/dist/services/shares.js +2 -1
  60. package/apps/runtime/dist/services/site.js +21 -10
  61. package/apps/runtime/dist/services/space-categories.js +7 -6
  62. package/apps/runtime/dist/services/space-data.js +19 -16
  63. package/apps/runtime/dist/services/spaces.js +8 -7
  64. package/apps/runtime/dist/services/wizards.js +41 -20
  65. package/apps/runtime/dist/store/errors.js +2 -1
  66. package/apps/runtime/dist/store/index.js +21 -4
  67. package/apps/runtime/dist/store/shared-lists.js +2 -3
  68. package/apps/runtime/dist/tools/index.js +17 -2
  69. package/apps/runtime/dist/tools/plugin.js +3 -2
  70. package/apps/runtime/dist/widgets/render.js +1 -1
  71. package/apps/runtime/dist/widgets/step.js +2 -1
  72. package/apps/web/dist/assets/{AboutDialog-nLn3vZAN.js → AboutDialog-i-qD_Nea.js} +1 -1
  73. package/apps/web/dist/assets/{ChangelogDialog-Cbx3mN2p.js → ChangelogDialog-BYuVcmKR.js} +1 -1
  74. package/apps/web/dist/assets/ChatView-DMEYEsA7.js +10 -0
  75. package/apps/web/dist/assets/EditorPage-Bry35WHx.js +3 -0
  76. package/apps/web/dist/assets/{FlowPreview-Bvj3R8g4.js → FlowPreview-BsfjDVv8.js} +1 -1
  77. package/apps/web/dist/assets/{Harness-D-Z3MbYq.js → Harness-BxQDTGJk.js} +2 -2
  78. package/apps/web/dist/assets/NewWizardChannels-DPnLT-md.js +1 -0
  79. package/apps/web/dist/assets/NewWizardPage-BSh_bUIJ.js +7 -0
  80. package/apps/web/dist/assets/{OnboardingPage-CdoAfnI2.js → OnboardingPage-BZrRdVwr.js} +1 -1
  81. package/apps/web/dist/assets/ProjectPage-_3L9R3SZ.js +1 -0
  82. package/apps/web/dist/assets/PublicRunner-DXbh8uhe.js +1 -0
  83. package/apps/web/dist/assets/{ReadOnly-DQPHmvS0.js → ReadOnly-DFysMEMF.js} +1 -1
  84. package/apps/web/dist/assets/RunDrawer-DpUakYb9.js +4 -0
  85. package/apps/web/dist/assets/RunnerView-DQ-NgcCa.js +1 -0
  86. package/apps/web/dist/assets/SettingsPage-DyUCIRjo.js +1 -0
  87. package/apps/web/dist/assets/SetupPage-BgVHvN6j.js +1 -0
  88. package/apps/web/dist/assets/SharePage-BI6vkc9c.js +1 -0
  89. package/apps/web/dist/assets/changelog-Cnv7ZZEL.js +1 -0
  90. package/apps/web/dist/assets/{circle-alert-DCBV7n5k.js → circle-alert-BVj6idzu.js} +1 -1
  91. package/apps/web/dist/assets/esm-hSq-jeOG.js +7 -0
  92. package/apps/web/dist/assets/index-Ca1841qE.js +14 -0
  93. package/apps/web/dist/assets/index-GSl2Rcjy.css +1 -0
  94. package/apps/web/dist/assets/{lock-DlzTfwxP.js → lock-BoWS-JhN.js} +1 -1
  95. package/apps/web/dist/assets/{phone-BFD815nc.js → phone-CeqyvwK-.js} +1 -1
  96. package/apps/web/dist/assets/settings-menu-BK8WpcGv.js +1 -0
  97. package/apps/web/dist/assets/ui-BbIh6SOJ.js +134 -0
  98. package/apps/web/dist/assets/{upload-BEf5-d6X.js → upload-BQrAlHIG.js} +1 -1
  99. package/apps/web/dist/index.html +4 -4
  100. package/apps/web/dist/mcp/flow.html +76 -76
  101. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +28 -4
  102. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +52 -1
  103. package/node_modules/@engenty-wizards/plugin-sdk/dist/thread.js +6 -1
  104. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +6 -2
  105. package/node_modules/@engenty-wizards/shared/dist/definition.js +13 -5
  106. package/node_modules/@engenty-wizards/shared/dist/knowledge.js +1 -1
  107. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +11 -1
  108. package/node_modules/@engenty-wizards/shared/dist/run.js +11 -1
  109. package/node_modules/@engenty-wizards/shared/dist/runners.d.ts +12 -0
  110. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +8 -0
  111. package/node_modules/@engenty-wizards/shared/dist/store.js +8 -0
  112. package/package.json +1 -1
  113. package/apps/web/dist/assets/ChatView-1VuHE2Q7.js +0 -10
  114. package/apps/web/dist/assets/EditorPage-Cp3jqQsk.js +0 -3
  115. package/apps/web/dist/assets/NewWizardChannels-DCF7JoL0.js +0 -1
  116. package/apps/web/dist/assets/NewWizardPage-BTlgq7yN.js +0 -7
  117. package/apps/web/dist/assets/ProjectPage-CoPSdDO9.js +0 -1
  118. package/apps/web/dist/assets/PublicRunner-CHwK3X75.js +0 -1
  119. package/apps/web/dist/assets/RunDrawer-CmGuqp0p.js +0 -4
  120. package/apps/web/dist/assets/RunnerView-DiaQiDbw.js +0 -1
  121. package/apps/web/dist/assets/SettingsPage-NzmFrJXb.js +0 -1
  122. package/apps/web/dist/assets/SetupPage-8mBuezdY.js +0 -1
  123. package/apps/web/dist/assets/SharePage-C-T5fkp1.js +0 -1
  124. package/apps/web/dist/assets/changelog-CsxPWKJ-.js +0 -1
  125. package/apps/web/dist/assets/esm-CnR8rDPi.js +0 -7
  126. package/apps/web/dist/assets/index-CmxVazOf.css +0 -1
  127. package/apps/web/dist/assets/index-D57rKyGN.js +0 -14
  128. package/apps/web/dist/assets/settings-menu-D-UqnL1p.js +0 -1
  129. package/apps/web/dist/assets/ui-DTuixH4y.js +0 -134
@@ -53,7 +53,8 @@ HOW YOU WORK
53
53
  - Finish with one to three sentences to the admin: what you built or changed, and at most one suggestion. If they only asked a question, just answer it.`;
54
54
  }
55
55
  /** Turns a tool call into the line the admin sees while it runs. */
56
- function activity(tool, args) {
56
+ function activity(tool, args, lang = "en") {
57
+ const de = lang === "de";
57
58
  const host = (u) => {
58
59
  try {
59
60
  return new URL(String(u)).hostname;
@@ -65,24 +66,42 @@ function activity(tool, args) {
65
66
  switch (tool) {
66
67
  case "replace_wizard":
67
68
  case "edit_wizard":
68
- return "Baut den Ablauf …";
69
+ return de ? "Baut den Ablauf …" : "Building the flow …";
69
70
  case "write_file":
70
- return args.path ? `Schreibt ${String(args.path)} …` : "Schreibt eine Datei …";
71
+ return args.path
72
+ ? de
73
+ ? `Schreibt ${String(args.path)} …`
74
+ : `Writing ${String(args.path)} …`
75
+ : de
76
+ ? "Schreibt eine Datei …"
77
+ : "Writing a file …";
71
78
  case "download_files":
72
- return "Lädt Daten herunter …";
79
+ return de ? "Lädt Daten herunter …" : "Downloading data …";
73
80
  case "web_fetch":
74
- return args.url ? `Liest ${host(args.url)} …` : "Liest im Web …";
81
+ return args.url
82
+ ? de
83
+ ? `Liest ${host(args.url)} …`
84
+ : `Reading ${host(args.url)} …`
85
+ : de
86
+ ? "Liest im Web …"
87
+ : "Reading the web …";
75
88
  case "web_search":
76
- return "Sucht im Web …";
89
+ return de ? "Sucht im Web …" : "Searching the web …";
77
90
  case "check_widget":
78
- return "Testet das Widget …";
91
+ return de ? "Testet das Widget …" : "Testing the widget …";
79
92
  case "run_script":
80
- return args.output ? `Bereitet ${String(args.output)} vor …` : "Rechnet …";
93
+ return args.output
94
+ ? de
95
+ ? `Bereitet ${String(args.output)} vor …`
96
+ : `Preparing ${String(args.output)} …`
97
+ : de
98
+ ? "Rechnet …"
99
+ : "Calculating …";
81
100
  case "list_files":
82
101
  case "read_file":
83
- return "Sieht sich die Dateien an …";
102
+ return de ? "Sieht sich die Dateien an …" : "Looking at the files …";
84
103
  case "delete_file":
85
- return "Räumt Dateien auf …";
104
+ return de ? "Räumt Dateien auf …" : "Tidying up files …";
86
105
  default:
87
106
  return null;
88
107
  }
@@ -410,7 +429,7 @@ export async function runArchitect(input) {
410
429
  }
411
430
  else if (chunk.type === "tool-call-input-streaming-start") {
412
431
  streaming.set(payload.toolCallId, { tool: payload.toolName, args: "", named: false });
413
- const label = activity(payload.toolName, {});
432
+ const label = activity(payload.toolName, {}, input.lang);
414
433
  if (label) {
415
434
  input.onActivity?.(label);
416
435
  }
@@ -422,7 +441,7 @@ export async function runArchitect(input) {
422
441
  const path = s.args.match(/"path"\s*:\s*"([^"]+)"/)?.[1];
423
442
  if (path) {
424
443
  s.named = true;
425
- const label = activity(s.tool, { path });
444
+ const label = activity(s.tool, { path }, input.lang);
426
445
  if (label) {
427
446
  input.onActivity?.(label);
428
447
  }
@@ -430,7 +449,7 @@ export async function runArchitect(input) {
430
449
  }
431
450
  }
432
451
  else if (chunk.type === "tool-call") {
433
- const label = activity(payload.toolName, (payload.args ?? payload.input ?? {}));
452
+ const label = activity(payload.toolName, (payload.args ?? payload.input ?? {}), input.lang);
434
453
  if (label) {
435
454
  input.onActivity?.(label);
436
455
  }
@@ -38,18 +38,18 @@ const SYSTEM = (wizardId) => [
38
38
  ].join("\n");
39
39
  const BUILDING = /edit_wizard|write_wizard|write_file|delete_file|create_wizard/;
40
40
  const ACTIVITY = [
41
- [/get_authoring_guide/, "Liest die Bauanleitung"],
42
- [/get_starter|list_starters/, "Sieht sich eine Vorlage an"],
43
- [/get_wizard|list_files|read_file/, "Liest den Wizard"],
44
- [/write_file|delete_file/, "Schreibt Dateien"],
45
- [/edit_wizard|write_wizard/, "Baut den Wizard um"],
46
- [/check_widget|preview/, "Prüft das Widget"],
47
- [/start_test_run|get_test_run/, "Testet den Wizard"],
48
- [/find_connectors|import_connector/, "Sucht einen Dienst"],
41
+ [/get_authoring_guide/, { de: "Liest die Bauanleitung", en: "Reading the building guide" }],
42
+ [/get_starter|list_starters/, { de: "Sieht sich eine Vorlage an", en: "Looking at a template" }],
43
+ [/get_wizard|list_files|read_file/, { de: "Liest den Wizard", en: "Reading the wizard" }],
44
+ [/write_file|delete_file/, { de: "Schreibt Dateien", en: "Writing files" }],
45
+ [/edit_wizard|write_wizard/, { de: "Baut den Wizard um", en: "Rebuilding the wizard" }],
46
+ [/check_widget|preview/, { de: "Prüft das Widget", en: "Checking the widget" }],
47
+ [/start_test_run|get_test_run/, { de: "Testet den Wizard", en: "Testing the wizard" }],
48
+ [/find_connectors|import_connector/, { de: "Sucht einen Dienst", en: "Looking for a service" }],
49
49
  ];
50
50
  /** The activity for one tool call, naming the file or service it is about when it says. */
51
- function activity(name, args) {
52
- const label = ACTIVITY.find(([re]) => re.test(name))?.[1] ?? "Arbeitet";
51
+ function activity(name, args, lang = "en") {
52
+ const label = ACTIVITY.find(([re]) => re.test(name))?.[1][lang] ?? (lang === "de" ? "Arbeitet" : "Working");
53
53
  const about = [args.path, args.query].find((v) => typeof v === "string" && v.trim());
54
54
  return about ? `${label}: ${String(about)} …` : `${label} …`;
55
55
  }
@@ -129,7 +129,7 @@ export async function subscriptionTurn(input) {
129
129
  if (event.type === "stream_event") {
130
130
  const e = event.event ?? {};
131
131
  if (e.type === "content_block_start" && e.content_block?.type === "tool_use") {
132
- input.onActivity(activity(String(e.content_block.name ?? ""), {}));
132
+ input.onActivity(activity(String(e.content_block.name ?? ""), {}, input.lang));
133
133
  }
134
134
  else if (e.type === "content_block_start" && e.content_block?.type === "thinking") {
135
135
  thinking = "";
@@ -156,7 +156,7 @@ export async function subscriptionTurn(input) {
156
156
  changed = true;
157
157
  input.onBuilding();
158
158
  }
159
- input.onActivity(activity(name, block.input ?? {}));
159
+ input.onActivity(activity(name, block.input ?? {}, input.lang));
160
160
  }
161
161
  }
162
162
  }
@@ -23,6 +23,7 @@ import { PLUGIN_NAME, pluginArchive, pluginMarketplace } from "./plugin.js";
23
23
  import { apiRoutes } from "./routes/api.js";
24
24
  import { browserReport, localDebugRoutes } from "./routes/debug.js";
25
25
  import { internalRoutes } from "./routes/internal.js";
26
+ import { inLang, requestLang } from "./routes/lang.js";
26
27
  import { marketplaceStudio } from "./routes/marketplace.js";
27
28
  import { publicPluginRoutes } from "./routes/plugin-public.js";
28
29
  import { pluginRoutes } from "./routes/plugins.js";
@@ -40,16 +41,16 @@ const app = new Hono();
40
41
  const { serves } = env;
41
42
  app.onError((err, c) => {
42
43
  if (err instanceof ServiceError) {
43
- return c.json(err.toJSON(), err.status);
44
+ return c.json(err.toJSON(requestLang(c)), err.status);
44
45
  }
45
46
  if (err instanceof ZodError) {
46
- return c.json({ error: "Ungültige Eingabe", issues: err.issues }, 400);
47
+ return c.json({ error: inLang(c, "Ungültige Eingabe", "Invalid input"), issues: err.issues }, 400);
47
48
  }
48
49
  if (err instanceof HTTPException) {
49
50
  return err.getResponse();
50
51
  }
51
52
  console.error(err);
52
- return c.json({ error: "Interner Fehler" }, 500);
53
+ return c.json({ error: inLang(c, "Interner Fehler", "Internal error") }, 500);
53
54
  });
54
55
  // --- who may talk to this server at all ---------------------------------------------
55
56
  // A runtime that runs alone listens on the loopback interface. A web page in the person's
@@ -203,7 +204,10 @@ if (serves.studio) {
203
204
  return c.json({ error: "unauthorized" }, 401);
204
205
  }
205
206
  if ((await tenantStatus(user.tenantId)) === "suspended") {
206
- return c.json({ error: "Dieses Konto ist gesperrt.", code: "suspended" }, 403);
207
+ return c.json({
208
+ error: inLang(c, "Dieses Konto ist gesperrt.", "This account is suspended."),
209
+ code: "suspended",
210
+ }, 403);
207
211
  }
208
212
  c.set("user", user);
209
213
  return withTenant(user.tenantId, () => asRole(user.role, next));
@@ -220,7 +224,7 @@ if (serves.runner) {
220
224
  // answers are text; an upload is a photo, a recording or a clip from a phone (routes/delivery.ts).
221
225
  const tooLarge = (max) => bodyLimit({
222
226
  maxSize: max,
223
- onError: (c) => c.json({ error: "Das ist zu groß.", code: "too_large" }, 413),
227
+ onError: (c) => c.json({ error: inLang(c, "Das ist zu groß.", "That is too large."), code: "too_large" }, 413),
224
228
  });
225
229
  const answers = tooLarge(4 * 1024 * 1024);
226
230
  const upload = tooLarge(48 * 1024 * 1024);
@@ -8,7 +8,7 @@ import { db, schema } from "../db/client.js";
8
8
  import { env } from "../env.js";
9
9
  import { detectHarnesses } from "../harness/index.js";
10
10
  import { managed, publicPlans, tenantInfo, tenantUsage } from "../manage.js";
11
- import { classWays } from "../models.js";
11
+ import { classWays, waysIn } from "../models.js";
12
12
  import { pluginToolsOf } from "../plugins/registry.js";
13
13
  import { earlierChats, readChat, searchChats } from "../services/assistant-chats.js";
14
14
  import { asLang, marketplaceDetail, searchMarketplace } from "../services/marketplace.js";
@@ -298,7 +298,7 @@ export function knowsTools(ctx) {
298
298
  execute: () => attempt(async () => {
299
299
  ctx.doing(ctx.lang === "de" ? "Sieht die Modelle nach …" : "Looking up the models …");
300
300
  return {
301
- classes: await classWays(),
301
+ classes: waysIn(await classWays(), "en"),
302
302
  clients: managed
303
303
  ? []
304
304
  : (await detectHarnesses()).map((h) => ({
@@ -4,29 +4,30 @@
4
4
  */
5
5
  export const EXAMPLE_WIZARD = {
6
6
  version: 1,
7
- title: "X-Post mit Bild",
8
- description: "Aus einem Thema wird ein fertiger Post mit Bild.",
7
+ title: "X post with an image",
8
+ description: "Turns a topic into a finished post with an image.",
9
9
  avatar: "flame",
10
- intro: "Erzähl mir kurz, worum es geht – ich schreibe den Post und zeichne das Bild dazu.",
10
+ lang: "en",
11
+ intro: "Tell me briefly what it is about – I write the post and draw the image for it.",
11
12
  steps: [
12
13
  {
13
14
  id: "about",
14
15
  type: "page",
15
- title: "Worum geht's?",
16
+ title: "What is it about?",
16
17
  fields: [
17
18
  {
18
19
  id: "topic",
19
- label: "Thema oder Anlass",
20
+ label: "Topic or occasion",
20
21
  kind: "textarea",
21
22
  required: true,
22
- placeholder: "z. B. Wir eröffnen am Freitag unser neues Studio in Wien",
23
+ placeholder: "e.g. We open our new studio in Vienna on Friday",
23
24
  },
24
25
  {
25
26
  id: "tone",
26
- label: "Ton",
27
+ label: "Tone",
27
28
  kind: "select",
28
- options: ["Locker", "Professionell", "Witzig", "Inspirierend"],
29
- default: "Locker",
29
+ options: ["Casual", "Professional", "Funny", "Inspiring"],
30
+ default: "Casual",
30
31
  },
31
32
  { id: "link", label: "Link (optional)", kind: "url" },
32
33
  ],
@@ -34,9 +35,9 @@ export const EXAMPLE_WIZARD = {
34
35
  {
35
36
  id: "post",
36
37
  type: "agent",
37
- title: "Post schreiben",
38
- working: "Schreibt den Post …",
39
- instructions: "Schreibe einen Post für X (Twitter) zum Thema: {{topic}}. Ton: {{tone}}. Höchstens 270 Zeichen inklusive Link und höchstens zwei Hashtags. Wenn ein Link angegeben ist ({{link}}), setze ihn ans Ende. Ein starker erster Satz. Nur den Post ausgeben.",
38
+ title: "Write the post",
39
+ working: "Writing the post …",
40
+ instructions: "Write a post for X (Twitter) on the topic: {{topic}}. Tone: {{tone}}. At most 270 characters including the link, and at most two hashtags. If a link is given ({{link}}), put it at the end. A strong first sentence. Output only the post.",
40
41
  tools: [],
41
42
  output: { format: "text" },
42
43
  model: "standard",
@@ -44,17 +45,17 @@ export const EXAMPLE_WIZARD = {
44
45
  {
45
46
  id: "visual",
46
47
  type: "generate",
47
- title: "Bild zeichnen",
48
- working: "Zeichnet das Bild …",
48
+ title: "Draw the image",
49
+ working: "Drawing the image …",
49
50
  asset: "image",
50
- prompt: "Ein auffälliges Bild, das diesen Post begleitet: {{steps.post}}. Thema: {{topic}}. Kein Text im Bild.",
51
+ prompt: "A striking image to go with this post: {{steps.post}}. Topic: {{topic}}. No text in the image.",
51
52
  options: { aspectRatio: "16:9", style: "modern editorial, clean, high contrast" },
52
53
  },
53
54
  {
54
55
  id: "check",
55
56
  type: "review",
56
- title: "Passt das so?",
57
- description: "Du kannst den Text direkt ändern oder eine neue Variante anfordern.",
57
+ title: "Does this work?",
58
+ description: "You can change the text directly or ask for a new variant.",
58
59
  show: ["post", "visual"],
59
60
  edit: true,
60
61
  regenerate: true,
@@ -62,11 +63,11 @@ export const EXAMPLE_WIZARD = {
62
63
  {
63
64
  id: "done",
64
65
  type: "result",
65
- title: "Fertig zum Posten",
66
- message: "Kopiere den Text und lade das Bild herunter.",
66
+ title: "Ready to post",
67
+ message: "Copy the text and download the image.",
67
68
  deliverables: [
68
69
  { from: "post", label: "Post", formats: ["txt", "md"] },
69
- { from: "visual", label: "Bild", formats: ["png"] },
70
+ { from: "visual", label: "Image", formats: ["png"] },
70
71
  ],
71
72
  },
72
73
  ],
@@ -13,6 +13,7 @@ type Wizard = {
13
13
  description: string // one sentence
14
14
  avatar: "round"|"drop"|"dome"|"flame"|"oval"|"bean"|"pebble"|"sprout"|"tower"|"wedge" // the engenty mascot shown to end users
15
15
  intro?: string // one friendly sentence above the first page
16
+ lang?: "en"|"de" // the language the wizard's texts are in — ALWAYS set it to the language you write them in
16
17
  lists?: List[] // tabular data the wizard KEEPS between runs — separately for each person who runs it, or for all with "shared"
17
18
  connections?: Connection[] // accounts the PERSON connects during a run (their mailbox); kept for their next run
18
19
  steps: Step[] // run top to bottom; the LAST step is always a "result"
@@ -148,7 +149,8 @@ type AgentStep = {
148
149
  working?: string // shown while it runs ("Recherchiert im Web …")
149
150
  }
150
151
  // Every agent step also has, without listing them: list_read / list_write (the wizard's lists), files_list,
151
- // read_document (PDF, scans and photos, Word, Excel, CSV, saved mails) and scan_documents (invoice / receipt fields, many at once);
152
+ // read_document (PDF, scans and photos, Word, Excel, CSV, saved mails) and scan_documents (invoice / receipt fields, many at once)
153
+ // — on an installed client only where there is something to read: tools or connections, uploads, lists or kept files;
152
154
  // and where the space has Wissen: project_search (a question, "where" on its typed Kategorien, hits checked for relevance),
153
155
  // project_list (by Kategorien), page_read / table_read / project_document (an item whole, by its path).
154
156
  // "pages" brings: page_read / page_write — Markdown pages of the space the wizard keeps for every run (notes, a log
@@ -249,7 +251,7 @@ Templates (in instructions/prompt): {{fieldId}} · {{steps.stepId}} (whole outpu
249
251
  A template may only use fields asked and steps run EARLIER.`;
250
252
  export const PRINCIPLES = `
251
253
  How good wizards look:
252
- - For non-technical end users. Plain words, friendly titles phrased as questions ("Wofür ist der Post?").
254
+ - For non-technical end users. Plain words, friendly titles phrased as questions ("What is the post for?").
253
255
  - A page whose fields depend on an earlier answer uses "when" on its fields, never one copy of the page per answer and a branch to each copy. Choices that depend on what an earlier step found use "optionsFrom", never one page per possible result.
254
256
  - Few pages, 1–5 fields each, one theme per page. Only ask what the result really needs; prefer selects with sensible defaults.
255
257
  - Put a review step before anything expensive (video) and before the final result, so people can correct or regenerate.
@@ -240,12 +240,12 @@ export const imapConnector = {
240
240
  kind: "api_key",
241
241
  apiKey: {
242
242
  fields: [
243
- { key: "email", label: "E-Mail-Adresse", placeholder: "name@firma.at" },
244
- { key: "password", label: "Passwort oder App-Passwort", secret: true },
243
+ { key: "email", label: "Email address", placeholder: "name@company.com" },
244
+ { key: "password", label: "Password or app password", secret: true },
245
245
  {
246
246
  key: "host",
247
- label: "IMAP-Server",
248
- placeholder: "leer lassen bei Gmail, GMX, iCloud …",
247
+ label: "IMAP server",
248
+ placeholder: "leave empty for Gmail, GMX, iCloud …",
249
249
  required: false,
250
250
  },
251
251
  { key: "port", label: "Port", placeholder: "993", required: false },
@@ -1,8 +1,10 @@
1
+ import { bilingual } from "@engenty-wizards/shared/run";
1
2
  import { eq } from "drizzle-orm";
2
3
  import { db, schema } from "../db/client.js";
3
4
  import { buildAuthorizationUrl, createOAuth2Pkce, exchangeAuthorizationCode, refreshAccessToken, } from "../engenty/connections-sdk/oauth2.js";
4
5
  import { scopesForGroups, } from "../engenty/connections-sdk/types.js";
5
6
  import { env } from "../env.js";
7
+ import { NotedError } from "../model-errors.js";
6
8
  import { seal, unseal } from "../secrets/crypto.js";
7
9
  import { deleteSecret, getSecret, putSecret } from "../store/index.js";
8
10
  import { connectorFetch, MAIL_CONNECTORS, resolveEnv, usable, viaAccount } from "./builtin.js";
@@ -11,7 +13,18 @@ import { AccountConnectError, redeemTicket, refreshThroughAccount, startThroughA
11
13
  /** The connectors a person can pick for each kind of connection. */
12
14
  const CONNECTORS = { mail: MAIL_CONNECTORS };
13
15
  const HINTS = {
14
- imap: "Bei Gmail, iCloud, GMX und web.de brauchst du ein App-Passwort (in den Sicherheitseinstellungen deines Kontos), nicht dein normales Passwort.",
16
+ imap: {
17
+ de: "Bei Gmail, iCloud, GMX und web.de brauchst du ein App-Passwort (in den Sicherheitseinstellungen deines Kontos), nicht dein normales Passwort.",
18
+ en: "Gmail, iCloud, GMX and web.de need an app password (in your account's security settings), not your normal password.",
19
+ },
20
+ };
21
+ /** The built-in connectors' fields in German; their definitions say them in English. */
22
+ const GERMAN_FIELDS = {
23
+ imap: {
24
+ email: { label: "E-Mail-Adresse", placeholder: "name@firma.at" },
25
+ password: { label: "Passwort oder App-Passwort" },
26
+ host: { label: "IMAP-Server", placeholder: "leer lassen bei Gmail, GMX, iCloud …" },
27
+ },
15
28
  };
16
29
  /**
17
30
  * What a connection asks the provider for: what reading needs, plus what each action the wizard
@@ -43,7 +56,7 @@ async function candidates(connection, projectId) {
43
56
  }
44
57
  return connection.kind ? CONNECTORS[connection.kind] : [];
45
58
  }
46
- export async function connectorOptions(connection, projectId) {
59
+ export async function connectorOptions(connection, projectId, lang = "en") {
47
60
  const out = [];
48
61
  for (const c of await candidates(connection, projectId)) {
49
62
  if (!(await usable(c))) {
@@ -54,8 +67,10 @@ export async function connectorOptions(connection, projectId) {
54
67
  id: c.id,
55
68
  name: c.name,
56
69
  auth: "api_key",
57
- fields: c.auth.apiKey.fields,
58
- hint: HINTS[c.id],
70
+ fields: lang === "de"
71
+ ? c.auth.apiKey.fields.map((f) => ({ ...f, ...GERMAN_FIELDS[c.id]?.[f.key] }))
72
+ : c.auth.apiKey.fields,
73
+ hint: HINTS[c.id]?.[lang],
59
74
  }
60
75
  : { id: c.id, name: c.name, auth: "oauth2" });
61
76
  }
@@ -68,7 +83,9 @@ async function connectorFor(connection, connectorId, projectId) {
68
83
  }
69
84
  return connector;
70
85
  }
71
- export async function connectionViews(connections, scope, projectId) {
86
+ export async function connectionViews(connections, scope, projectId,
87
+ /** The language the person reads the connectors' fields in. */
88
+ lang = "en") {
72
89
  return Promise.all(connections.map(async (c) => {
73
90
  const stored = await getSecret(scope, slot(c.id));
74
91
  const only = c.connector ? (await candidates(c, projectId))[0] : undefined;
@@ -87,11 +104,11 @@ export async function connectionViews(connections, scope, projectId) {
87
104
  : only && open(only)
88
105
  ? { connector: only.id, label: only.name, connectedAt: new Date(0).toISOString() }
89
106
  : null,
90
- connectors: await connectorOptions(c, projectId),
107
+ connectors: await connectorOptions(c, projectId, lang),
91
108
  };
92
109
  }));
93
110
  }
94
- export async function startOAuth(scope, projectId, runId, connection, connectorId) {
111
+ export async function startOAuth(scope, projectId, runId, connection, connectorId, lang) {
95
112
  const connector = await connectorFor(connection, connectorId, projectId);
96
113
  if (connector.auth.kind !== "oauth2") {
97
114
  throw new Error(`${connector.name} is not connected by signing in.`);
@@ -106,6 +123,7 @@ export async function startOAuth(scope, projectId, runId, connection, connectorI
106
123
  connectorId,
107
124
  verifier: "",
108
125
  exp: Date.now() + 15 * 60_000,
126
+ lang,
109
127
  };
110
128
  try {
111
129
  return await startThroughAccount(scopesFor(connection, connector), seal(state), connectRedirectUri());
@@ -125,6 +143,7 @@ export async function startOAuth(scope, projectId, runId, connection, connectorI
125
143
  connectorId,
126
144
  verifier: pkce.codeVerifier,
127
145
  exp: Date.now() + 15 * 60_000,
146
+ lang,
128
147
  };
129
148
  return buildAuthorizationUrl({
130
149
  connector,
@@ -135,6 +154,11 @@ export async function startOAuth(scope, projectId, runId, connection, connectorI
135
154
  resolveEnv,
136
155
  });
137
156
  }
157
+ /** The language a sign-in was started in, a wizard's or a plugin's; null where it names none. */
158
+ export function oauthStateLang(rawState) {
159
+ const lang = unseal(rawState)?.lang;
160
+ return lang === "de" || lang === "en" ? lang : null;
161
+ }
138
162
  /** The run an OAuth callback belongs to; its tenant is looked up before the state is used. */
139
163
  export function oauthStateRun(rawState) {
140
164
  return unseal(rawState)?.runId ?? null;
@@ -146,7 +170,7 @@ export function oauthStateRun(rawState) {
146
170
  export async function finishOAuth(answer, rawState) {
147
171
  const state = unseal(rawState);
148
172
  if (!state || state.exp < Date.now()) {
149
- throw new Error("Die Anmeldung ist abgelaufen. Bitte noch einmal verbinden.");
173
+ throw new Error("The sign-in has expired. Please connect again.");
150
174
  }
151
175
  const run = await db.query.run.findFirst({ where: eq(schema.run.id, state.runId) });
152
176
  const connection = run?.definition.connections?.find((c) => c.id === state.connectionId);
@@ -190,7 +214,7 @@ export async function connectWithCredentials(scope, projectId, connection, conne
190
214
  for (const field of connector.auth.apiKey.fields) {
191
215
  const value = String(values[field.key] ?? "").trim();
192
216
  if (!value && field.required !== false) {
193
- throw new ConnectError(`„${field.label}“ fehlt.`);
217
+ throw new ConnectError(bilingual(`„${field.label}“ fehlt.`, `“${field.label}” is missing.`));
194
218
  }
195
219
  if (value) {
196
220
  credentials[field.key] = value.slice(0, 500);
@@ -213,22 +237,23 @@ export async function connectWithCredentials(scope, projectId, connection, conne
213
237
  return account.label;
214
238
  }
215
239
  /** A connect attempt the person can fix (wrong password, unknown server). */
216
- export class ConnectError extends Error {
240
+ export class ConnectError extends NotedError {
217
241
  }
218
242
  function connectFailure(err) {
219
243
  const e = err;
220
244
  const host = e.imapHost ? ` (${e.imapHost})` : "";
221
245
  if (e.authenticationFailed) {
222
- const server = e.responseText ? ` Antwort des Servers: ${e.responseText.slice(0, 160)}` : "";
223
- return `Anmeldung abgelehnt${host}. Stimmen Adresse und (App-)Passwort? Bei Gmail und Google Workspace braucht es ein App-Passwort und aktiviertes IMAP.${server}`;
246
+ const answer = e.responseText?.slice(0, 160);
247
+ return bilingual(`Anmeldung abgelehnt${host}. Stimmen Adresse und (App-)Passwort? Bei Gmail und Google Workspace braucht es ein App-Passwort und aktiviertes IMAP.${answer ? ` Antwort des Servers: ${answer}` : ""}`, `Sign-in refused${host}. Are the address and (app) password right? Gmail and Google Workspace need an app password and IMAP switched on.${answer ? ` The server answered: ${answer}` : ""}`);
224
248
  }
225
249
  if (e.code === "ENOTFOUND") {
226
- return `Den Mail-Server${host} gibt es nicht. Bitte den IMAP-Server eintragen, bei Gmail und Google Workspace imap.gmail.com.`;
250
+ return bilingual(`Den Mail-Server${host} gibt es nicht. Bitte den IMAP-Server eintragen, bei Gmail und Google Workspace imap.gmail.com.`, `The mail server${host} does not exist. Please enter the IMAP server; for Gmail and Google Workspace it is imap.gmail.com.`);
227
251
  }
228
252
  if (e.code === "ETIMEDOUT" || e.code === "ETIMEOUT" || e.code === "ECONNREFUSED") {
229
- return `Der Mail-Server${host} antwortet nicht. Bitte IMAP-Server und Port prüfen.`;
253
+ return bilingual(`Der Mail-Server${host} antwortet nicht. Bitte IMAP-Server und Port prüfen.`, `The mail server${host} does not answer. Please check the IMAP server and port.`);
230
254
  }
231
- return `Verbinden hat nicht geklappt${host}: ${String(e.message ?? err).slice(0, 200)}`;
255
+ const detail = String(e.message ?? err).slice(0, 200);
256
+ return bilingual(`Verbinden hat nicht geklappt${host}: ${detail}`, `Connecting did not work${host}: ${detail}`);
232
257
  }
233
258
  export function disconnect(scope, connectionId) {
234
259
  return deleteSecret(scope, slot(connectionId));
@@ -101,7 +101,7 @@ export function pluginStateTenant(rawState) {
101
101
  export async function finishPluginOAuth(answer, rawState) {
102
102
  const state = unseal(rawState);
103
103
  if (!state?.plugin || state.exp < Date.now()) {
104
- throw new Error("Die Anmeldung ist abgelaufen. Bitte noch einmal verbinden.");
104
+ throw new Error("The sign-in has expired. Please connect again.");
105
105
  }
106
106
  const connector = oauthConnector(state.connectorId);
107
107
  if (connector.auth.kind !== "oauth2") {
@@ -222,7 +222,7 @@ export function pluginConnections(plugin) {
222
222
  }
223
223
  return out;
224
224
  },
225
- async start({ space, connector: connectorId, actions }) {
225
+ async start({ space, connector: connectorId, actions, lang }) {
226
226
  const connector = oauthConnector(connectorId);
227
227
  if (connector.auth.kind !== "oauth2") {
228
228
  throw new Error("not an oauth connector");
@@ -236,6 +236,7 @@ export function pluginConnections(plugin) {
236
236
  connectorId,
237
237
  actions: [...new Set(actions)],
238
238
  exp: Date.now() + 15 * 60_000,
239
+ lang,
239
240
  };
240
241
  if (await viaAccount(connector)) {
241
242
  // The Manage-App keeps the PKCE verifier; ours stays empty.
@@ -1,5 +1,7 @@
1
+ import { bilingual } from "@engenty-wizards/shared/run";
1
2
  import { accountToken } from "../auth/account.js";
2
3
  import { env } from "../env.js";
4
+ import { NotedError } from "../model-errors.js";
3
5
  /*
4
6
  * Google accounts on a local install without an OAuth client of its own: the Manage-App of the
5
7
  * linked account runs the consent at Google with its client and hands the tokens over, and
@@ -7,7 +9,7 @@ import { env } from "../env.js";
7
9
  */
8
10
  const base = () => `${env.local.accountUrl}/v1/connect`;
9
11
  /** Something the person can mend or retry; the message is for them. */
10
- export class AccountConnectError extends Error {
12
+ export class AccountConnectError extends NotedError {
11
13
  }
12
14
  let offered = null;
13
15
  /** Whether the Manage-App connects Google accounts; asked at most every ten minutes. */
@@ -24,7 +26,7 @@ export async function googleThroughAccount() {
24
26
  async function call(path, body) {
25
27
  const token = await accountToken();
26
28
  if (!token) {
27
- throw new AccountConnectError("Verbinde zuerst dein engenty-Konto (Einstellungen → Konto), dann lässt sich Google hier verbinden.");
29
+ throw new AccountConnectError(bilingual("Verbinde zuerst dein engenty-Konto (Einstellungen → Konto), dann lässt sich Google hier verbinden.", "Connect your engenty account first (Settings → Account); then Google can be connected here."));
28
30
  }
29
31
  const res = await fetch(`${base()}${path}`, {
30
32
  method: "POST",
@@ -33,15 +35,15 @@ async function call(path, body) {
33
35
  signal: AbortSignal.timeout(20_000),
34
36
  }).catch(() => null);
35
37
  if (!res) {
36
- throw new AccountConnectError("Dein engenty-Konto ist gerade nicht erreichbar. Bitte später noch einmal.");
38
+ throw new AccountConnectError(bilingual("Dein engenty-Konto ist gerade nicht erreichbar. Bitte später noch einmal.", "Your engenty account cannot be reached right now. Please try again later."));
37
39
  }
38
40
  const data = (await res.json().catch(() => ({})));
39
41
  if (!res.ok) {
40
42
  throw new AccountConnectError(data.code === "invalid_grant"
41
- ? "Google nimmt diese Verbindung nicht mehr an. Bitte das Konto neu verbinden."
43
+ ? bilingual("Google nimmt diese Verbindung nicht mehr an. Bitte das Konto neu verbinden.", "Google no longer accepts this connection. Please connect the account again.")
42
44
  : data.code === "not_configured"
43
- ? "Google lässt sich über dein engenty-Konto gerade nicht verbinden."
44
- : `Verbinden über dein engenty-Konto hat nicht geklappt (${data.code ?? res.status}).`);
45
+ ? bilingual("Google lässt sich über dein engenty-Konto gerade nicht verbinden.", "Google cannot be connected through your engenty account right now.")
46
+ : bilingual(`Verbinden über dein engenty-Konto hat nicht geklappt (${data.code ?? res.status}).`, `Connecting through your engenty account did not work (${data.code ?? res.status}).`));
45
47
  }
46
48
  return data;
47
49
  }
@@ -29,7 +29,7 @@ const { events } = await server.connections.call<{ events: unknown[] }>(account.
29
29
  | `server.connections.` | Does |
30
30
  |---|---|
31
31
  | `available(ids)` | Of these connector ids, the ones a person can connect here now: `{ id, name, auth }`, and for `auth: "api_key"` the `fields` to ask for (`key`, `label`, `placeholder`, `required`, `secret`). An OAuth connector is offered once its client is set up (`GOOGLE_OAUTH_CLIENT_ID` …), and on a local install Google also through the linked account; a connector that takes keys (HubSpot, S3) always |
32
- | `start({ space, connector, actions })` | An OAuth connector: the provider's sign-in page. Only the rights `actions` need are asked for |
32
+ | `start({ space, connector, actions, lang? })` | An OAuth connector: the provider's sign-in page. Only the rights `actions` need are asked for. `lang` (`"en"` or `"de"`) is the language of the page the person comes back to; without it, their browser's |
33
33
  | `connect({ space, connector, actions, credentials })` | A connector that takes keys: the keys by field key, tried once at the provider, then kept sealed. Keys the provider refuses throw an error with the status `400` and the code `credentials`, whose message says what was wrong. Answers the connection |
34
34
  | `list(space)`, `get(id)` | What is connected: `{ id, space, connector, label, actions, createdAt }`, the newest first. `label` is the account as the provider names it |
35
35
  | `call(id, action, input)` | Runs one of the connector's actions with the account. A fresh token is fetched first when the old one runs out |
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Reference
3
3
  description: Every call, type, address and setting of the plugin framework on one page.
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  ---
6
6
 
7
7
  ## Server half
@@ -43,7 +43,7 @@ The default export is a `WizardsPluginFactory`: `(wizards: WizardsPluginApi) =>
43
43
  | `publicRef(): Promise<string>` | [Routes](./routes.md#shared-routes). The part of `publicUrl` that names the current tenant |
44
44
  | `registerSharedRoute(route: PluginSharedRoute)` | [Routes](./routes.md#shared-routes). One route for every tenant; `handler({ request, path, params, query, inTenant })` |
45
45
  | `sharedUrl(path): string` | The full address of a shared route, the same for every tenant |
46
- | `channels.get()`, `.request({ userId, country, wish?, lang })`, `.withdraw()`, `.book({ kind, quantity, key, runId? })` | [Numbers of the Manage-App](./server.md#numbers-of-the-manage-app). `get`: `{ number, request, shared }` or `null` without a Manage-App |
46
+ | `channels.get()`, `.request({ userId, country, wish?, lang })`, `.withdraw()`, `.book({ kind, quantity, key, runId? })` | [Numbers of the Manage-App](./server.md#numbers-of-the-manage-app). `get`: `{ number, request, shared, allowed }` or `null` without a Manage-App |
47
47
  | `registerMigrations(folder: string)` | [Tables](./tables.md). The folder is relative to the plugin's |
48
48
  | `on(event, listener)` | [Events](./events.md). `event`: `"run.done"`, `"run.failed"`, `"run.cancelled"`, `"space.created"`, `"space.updated"`, `"space.deleted"`, `"space.file.ready"`, `"space.file.removed"` |
49
49
  | `every(name, everyMs, handler)` | [Jobs](./server.md#jobs). Per tenant that has the plugin, at most once per `everyMs` (a minute at least). `handler({ tenantId, lastRun, signal })` |
@@ -243,6 +243,7 @@ The default export is a `StudioPlugin`: `(studio: StudioPluginContext) => void`.
243
243
  | `studio.registerNav({ to, label, icon })` | An icon in the app bar. `label: () => string` |
244
244
  | `studio.registerSettingsSection({ id, label, icon, component, menu? })` | A section at `/studio/settings/<id>` |
245
245
  | `studio.registerShareSection({ runner, component })` | [Runners](./runners.md#the-runners-part-of-the-share-dialog). The runner's own settings for a wizard (a keyword), under the share dialog's gear once it is switched on and set up. `component` gets `{ wizard: { id, title } }` |
246
+ | `studio.registerShareSetup({ runner, title, component, notice? })` | [Runners](./runners.md#setting-up-inside-the-dialog). What "Einrichten" in the runner's row opens inside the share dialog. `component` gets `{ wizard, Actions, done(banner?) }`, `notice` `{ wizard, open }` |
246
247
  | `studio.registerAssistantCard({ tool, component })` | Draws a tool's result in the space assistant's chat. `component` gets `{ data, send }` |
247
248
  | `studio.registerSpaceSection({ id, group?, label, hint?, component })` | A section of a part of the space page at `/studio/space/<group>#<id>`; `group`: `info`, `knowledge` (default), `data` or `results` |
248
249
  | `studio.registerSpacePart({ id, label, icon, after?, component })` | [A part of the plugin's own](./studio.md#a-part-of-the-plugins-own) at `/studio/space/<id>`, after `info`, `knowledge` or `data` (default) in the menu |