wizards 0.6.1 → 0.7.0

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 (97) hide show
  1. package/README.md +6 -2
  2. package/apps/runtime/dist/agents/architect.js +7 -29
  3. package/apps/runtime/dist/agents/studio-assistant.js +7 -24
  4. package/apps/runtime/dist/authoring/guide.js +1 -1
  5. package/apps/runtime/dist/cli/autostart.js +58 -12
  6. package/apps/runtime/dist/cli/autoupdate.js +221 -0
  7. package/apps/runtime/dist/cli/environment.js +5 -0
  8. package/apps/runtime/dist/cli/main.js +9 -2
  9. package/apps/runtime/dist/cli/menu.js +2 -2
  10. package/apps/runtime/dist/cli/processes.js +72 -0
  11. package/apps/runtime/dist/cli/setup.js +9 -4
  12. package/apps/runtime/dist/cli/start.js +18 -1
  13. package/apps/runtime/dist/cli/status.js +51 -17
  14. package/apps/runtime/dist/cli/uninstall-main.js +20 -0
  15. package/apps/runtime/dist/cli/uninstall.js +231 -0
  16. package/apps/runtime/dist/cli/update.js +25 -2
  17. package/apps/runtime/dist/cli/versions.js +106 -0
  18. package/apps/runtime/dist/docs/dev/wizard-format.md +6 -4
  19. package/apps/runtime/dist/docs/user/command-line.md +2 -1
  20. package/apps/runtime/dist/docs/user/data.md +1 -1
  21. package/apps/runtime/dist/docs/user/install.md +29 -6
  22. package/apps/runtime/dist/engine/input.js +4 -5
  23. package/apps/runtime/dist/engine/runner.js +5 -5
  24. package/apps/runtime/dist/engine/steps.js +5 -5
  25. package/apps/runtime/dist/engine/template.js +10 -15
  26. package/apps/runtime/dist/env.js +3 -20
  27. package/apps/runtime/dist/features.js +2 -2
  28. package/apps/runtime/dist/index.js +3 -1
  29. package/apps/runtime/dist/manage.js +7 -0
  30. package/apps/runtime/dist/media/conversation.js +2 -2
  31. package/apps/runtime/dist/models.js +12 -0
  32. package/apps/runtime/dist/plugins/runs.js +4 -4
  33. package/apps/runtime/dist/routes/runs.js +4 -2
  34. package/apps/runtime/dist/sandbox/docker.js +0 -2
  35. package/apps/runtime/dist/sandbox/index.js +3 -10
  36. package/apps/runtime/dist/services/api.js +2 -2
  37. package/apps/runtime/dist/services/runners.js +2 -2
  38. package/apps/runtime/dist/services/space-categories.js +3 -25
  39. package/apps/runtime/dist/services/translations.js +7 -4
  40. package/apps/runtime/dist/tools/browser.js +2 -14
  41. package/apps/runtime/dist/tools/index.js +1 -19
  42. package/apps/runtime/dist/tools/mail.js +1 -4
  43. package/apps/runtime/dist/tools/shared.js +30 -3
  44. package/apps/runtime/dist/update.js +10 -19
  45. package/apps/web/dist/assets/AboutDialog-CUVmdhzk.js +1 -0
  46. package/apps/web/dist/assets/{ChangelogDialog-jQk_bQpP.js → ChangelogDialog-DcQqFbiW.js} +1 -1
  47. package/apps/web/dist/assets/ChatView-DTPWjzWF.js +10 -0
  48. package/apps/web/dist/assets/EditorPage-SJHtft7P.js +3 -0
  49. package/apps/web/dist/assets/{FlowPreview-DA0sHtr7.js → FlowPreview-DvpjuQdC.js} +1 -1
  50. package/apps/web/dist/assets/{Harness-DmYkwU4x.js → Harness-D6fezoWO.js} +2 -2
  51. package/apps/web/dist/assets/{NewWizardChannels-BLS9sKhT.js → NewWizardChannels-BHeutZ7V.js} +1 -1
  52. package/apps/web/dist/assets/NewWizardPage-CzBxaPhH.js +7 -0
  53. package/apps/web/dist/assets/{OnboardingPage-CREvCCdX.js → OnboardingPage-DbR4N2Ms.js} +1 -1
  54. package/apps/web/dist/assets/ProjectPage-DG6_E7kA.js +1 -0
  55. package/apps/web/dist/assets/PublicRunner-19tV_8TR.js +1 -0
  56. package/apps/web/dist/assets/{ReadOnly-b7TjjRAk.js → ReadOnly-BMXFtKs0.js} +1 -1
  57. package/apps/web/dist/assets/RunDrawer-DqwLjFJb.js +4 -0
  58. package/apps/web/dist/assets/{RunnerView-CLfZzbKn.js → RunnerView-DRc4J-rg.js} +1 -1
  59. package/apps/web/dist/assets/SettingsPage-CCypPS9L.js +1 -0
  60. package/apps/web/dist/assets/SetupPage-D2Y2rev4.js +1 -0
  61. package/apps/web/dist/assets/{SharePage-B21QI97K.js → SharePage-DB5n6Oug.js} +1 -1
  62. package/apps/web/dist/assets/changelog-BDhd7tjO.js +1 -0
  63. package/apps/web/dist/assets/{esm-DqRvG-PU.js → esm-6TNBQNCD.js} +1 -1
  64. package/apps/web/dist/assets/index-mBRI7ZQp.js +14 -0
  65. package/apps/web/dist/assets/{lock-D_KjsGfU.js → lock-HLZib0u9.js} +1 -1
  66. package/apps/web/dist/assets/{phone-YRxkPQk6.js → phone-DsxbIcWi.js} +1 -1
  67. package/apps/web/dist/assets/settings-menu-BMMkIVP1.js +1 -0
  68. package/apps/web/dist/assets/ui-Brg2qM1A.js +137 -0
  69. package/apps/web/dist/assets/{upload-DDo14e2p.js → upload-BaGy0ppG.js} +1 -1
  70. package/apps/web/dist/index.html +2 -2
  71. package/apps/web/dist/install.sh +29 -11
  72. package/apps/web/dist/mcp/flow.html +11 -11
  73. package/bin/wizards.mjs +17 -3
  74. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +18 -7
  75. package/node_modules/@engenty-wizards/shared/dist/definition.js +50 -21
  76. package/node_modules/@engenty-wizards/shared/dist/features.d.ts +6 -0
  77. package/node_modules/@engenty-wizards/shared/dist/features.js +12 -0
  78. package/node_modules/@engenty-wizards/shared/dist/numbers.d.ts +24 -0
  79. package/node_modules/@engenty-wizards/shared/dist/numbers.js +54 -0
  80. package/node_modules/@engenty-wizards/shared/dist/run.d.ts +4 -4
  81. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +7 -0
  82. package/node_modules/@engenty-wizards/shared/dist/store.js +28 -0
  83. package/package.json +1 -1
  84. package/apps/runtime/dist/sandbox/agentos.js +0 -191
  85. package/apps/web/dist/assets/AboutDialog-CTuI-l3p.js +0 -1
  86. package/apps/web/dist/assets/ChatView-9TLi0B7K.js +0 -10
  87. package/apps/web/dist/assets/EditorPage-BVrzOfF3.js +0 -3
  88. package/apps/web/dist/assets/NewWizardPage-DWYZYiBF.js +0 -7
  89. package/apps/web/dist/assets/ProjectPage-BQkd55xR.js +0 -1
  90. package/apps/web/dist/assets/PublicRunner-sOlOC0zD.js +0 -1
  91. package/apps/web/dist/assets/RunDrawer-BPIVaan7.js +0 -4
  92. package/apps/web/dist/assets/SettingsPage-BO6TPmoj.js +0 -1
  93. package/apps/web/dist/assets/SetupPage-DlJHBYWH.js +0 -1
  94. package/apps/web/dist/assets/changelog-DeSoCsai.js +0 -1
  95. package/apps/web/dist/assets/index-CAUeJXCg.js +0 -14
  96. package/apps/web/dist/assets/settings-menu-CQgehwEX.js +0 -1
  97. package/apps/web/dist/assets/ui-DDUCc_4v.js +0 -137
package/bin/wizards.mjs CHANGED
@@ -24,7 +24,8 @@ const home = join(
24
24
  );
25
25
  const runtime = join(home, "runtime");
26
26
  const wrapper = join(home, "bin", "wizards");
27
- const RUN_HERE = ["help", "validate", "mcp"];
27
+ // uninstall: never installs or hands over first; the install it takes away may no longer run.
28
+ const RUN_HERE = ["help", "validate", "mcp", "uninstall"];
28
29
  const tilde = (path) =>
29
30
  path.startsWith(homedir() + sep) ? `~${path.slice(homedir().length)}` : path;
30
31
 
@@ -164,7 +165,7 @@ if (process.platform === "win32" && !checkout && !RUN_HERE.includes(command)) {
164
165
  process.exit(1);
165
166
  }
166
167
  const fromNpm = process.platform !== "win32" && !checkout && !isInstall();
167
- if (fromNpm && (!currentNode || !RUN_HERE.includes(command))) {
168
+ if (fromNpm && command !== "uninstall" && (!currentNode || !RUN_HERE.includes(command))) {
168
169
  const own = versionOf(root) || "0.0.0";
169
170
  const installed = existsSync(wrapper)
170
171
  ? versionOf(join(runtime, "node_modules", "wizards"))
@@ -194,6 +195,14 @@ if (fromNpm && (!currentNode || !RUN_HERE.includes(command))) {
194
195
  process.exit(await handOver());
195
196
  }
196
197
  }
198
+ if (!currentNode && command === "uninstall") {
199
+ // The install's own Node, if it is still there, runs this copy's uninstall.
200
+ const node = join(home, "tools", "node", "bin", "node");
201
+ const result = existsSync(node)
202
+ ? spawnSync(node, [fileURLToPath(import.meta.url), ...args], { stdio: "inherit" })
203
+ : null;
204
+ process.exit(result?.status ?? 1);
205
+ }
197
206
  if (!currentNode) {
198
207
  console.error(
199
208
  `engenty wizards needs Node.js 24.11 or newer. This is ${process.version}.\n` +
@@ -202,4 +211,9 @@ if (!currentNode) {
202
211
  );
203
212
  process.exit(1);
204
213
  }
205
- await import("../apps/runtime/dist/cli/main.js");
214
+ // The uninstall has an entry of its own: main.js makes the install and data folders as it loads.
215
+ await import(
216
+ command === "uninstall"
217
+ ? "../apps/runtime/dist/cli/uninstall-main.js"
218
+ : "../apps/runtime/dist/cli/main.js"
219
+ );
@@ -20,6 +20,11 @@ export declare const SPEECH_LOCALES: Record<Lang, string>;
20
20
  export declare function isLang(value: unknown): value is Lang;
21
21
  /** The language of a tag such as "de-AT", "fr" or "pt_BR", where engenty speaks it. */
22
22
  export declare function langOf(tag: string | null | undefined): Lang | null;
23
+ /**
24
+ * The languages a wizard is offered in when its author picks one: English, German and the main
25
+ * languages of Europe. A wizard may name any other real language by its code as well.
26
+ */
27
+ export declare const WIZARD_LANGS: readonly string[];
23
28
  /**
24
29
  * The person's own language beside English: the first of their languages (the browser's, in
25
30
  * its order) that engenty speaks, else the main language of the country their time zone lies
@@ -33,6 +38,13 @@ export declare function localLang(languages: readonly string[], timeZone?: strin
33
38
  export declare const LANG_CODE: RegExp;
34
39
  /** The code of a tag such as "fr-CA" or "pt_BR"; null where it names no language. */
35
40
  export declare function langCodeOf(tag: string | null | undefined): string | null;
41
+ /**
42
+ * A real language, as the platform's own language data knows it: "it" and "nb" are, "xx" is
43
+ * not. The schema only checks the shape of a code; this checks that it names a language.
44
+ */
45
+ export declare function isRealLanguage(code: string): boolean;
46
+ /** The language engenty's own strings are read in for any language: itself where they are written, else English. */
47
+ export declare function textLang(code: string): Lang;
36
48
  /**
37
49
  * The language the person reads, for a wizard translated on request: the first of their
38
50
  * languages (the browser's, in its order), else the main language of the country their time
@@ -45,6 +57,8 @@ export declare function langName(code: string): string;
45
57
  export declare function langNameEn(code: string): string;
46
58
  /** The locale amounts, dates and plural forms are written in for any language. */
47
59
  export declare function localeOf(code: string): string;
60
+ /** The locale speech is recognised and read aloud in for any language. */
61
+ export declare function speechLocaleOf(code: string): string;
48
62
  export declare const FIELD_KINDS: readonly ["text", "textarea", "number", "select", "multiselect", "date", "email", "url", "toggle", "color", "image", "file", "items", "connection", "list", "location", "audio", "signature", "slot"];
49
63
  export type FieldKind = (typeof FIELD_KINDS)[number];
50
64
  /**
@@ -1937,10 +1951,7 @@ export declare const wizardSchema: z.ZodObject<{
1937
1951
  wedge: "wedge";
1938
1952
  }>>;
1939
1953
  intro: z.ZodOptional<z.ZodString>;
1940
- lang: z.ZodOptional<z.ZodEnum<{
1941
- de: "de";
1942
- en: "en";
1943
- }>>;
1954
+ lang: z.ZodOptional<z.ZodString>;
1944
1955
  lists: z.ZodOptional<z.ZodArray<z.ZodObject<{
1945
1956
  id: z.ZodString;
1946
1957
  title: z.ZodString;
@@ -2884,9 +2895,9 @@ export interface SlotTime {
2884
2895
  */
2885
2896
  export declare function slotTime(iso: string): SlotTime | null;
2886
2897
  /** The weekday and date of a slot's day: "Montag, 12. Oktober" / "Monday, 12 October". */
2887
- export declare function slotDayText(iso: string, lang?: Lang): string;
2898
+ export declare function slotDayText(iso: string, lang?: string): string;
2888
2899
  /** A chosen slot as a person reads it back: "Di 13.10.2026, 10:30" / "Tue 13/10/2026, 10:30". */
2889
- export declare function slotText(iso: string, lang?: Lang): string;
2900
+ export declare function slotText(iso: string, lang?: string): string;
2890
2901
  /**
2891
2902
  * The slot among `options` that is the same moment as `value`, written as offered — so an answer
2892
2903
  * in another offset ("…T07:00:00Z" for "…T09:00:00+02:00") picks the offered time.
@@ -2906,6 +2917,6 @@ export declare function allFields(def: WizardDefinition): Field[];
2906
2917
  * The language a wizard speaks to people: the one it names, else read from its own words
2907
2918
  * (titles, labels, intro) — English where they are not clearly German.
2908
2919
  */
2909
- export declare function wizardLang(def: WizardDefinition): Lang;
2920
+ export declare function wizardLang(def: WizardDefinition): string;
2910
2921
  /** True when this app's schema reads the definition without losing anything of it. */
2911
2922
  export declare function readable(definition: unknown): boolean;
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: FSL-1.1-MIT OR MIT
2
2
  import { z } from "zod";
3
3
  import { tableColumnsSchema } from "./engenty/data-tables/index.js";
4
+ import { readNumber } from "./numbers.js";
4
5
  import { outputShapeSchema, shapeOfFields } from "./shape.js";
5
6
  import { CONNECTION_KINDS } from "./store.js";
6
7
  import { surfaceComponentsSchema, validateSurface } from "./surface.js";
@@ -81,6 +82,13 @@ const COUNTRY_LANGUAGE = {
81
82
  UA: "uk",
82
83
  VA: "it",
83
84
  };
85
+ /**
86
+ * The languages a wizard is offered in when its author picks one: English, German and the main
87
+ * languages of Europe. A wizard may name any other real language by its code as well.
88
+ */
89
+ export const WIZARD_LANGS = [
90
+ ...new Set(["en", "de", ...Object.values(COUNTRY_LANGUAGE)]),
91
+ ];
84
92
  /** The country of each European time zone (IANA names, as `Intl` gives them). */
85
93
  const ZONE_COUNTRY = {
86
94
  "Europe/Amsterdam": "NL",
@@ -151,6 +159,25 @@ export function langCodeOf(tag) {
151
159
  const code = tag?.trim().toLowerCase().split(/[-_]/)[0] ?? "";
152
160
  return LANG_CODE.test(code) ? code : null;
153
161
  }
162
+ /**
163
+ * A real language, as the platform's own language data knows it: "it" and "nb" are, "xx" is
164
+ * not. The schema only checks the shape of a code; this checks that it names a language.
165
+ */
166
+ export function isRealLanguage(code) {
167
+ if (!LANG_CODE.test(code)) {
168
+ return false;
169
+ }
170
+ try {
171
+ return Boolean(new Intl.DisplayNames(["en"], { type: "language", fallback: "none" }).of(code));
172
+ }
173
+ catch {
174
+ return false;
175
+ }
176
+ }
177
+ /** The language engenty's own strings are read in for any language: itself where they are written, else English. */
178
+ export function textLang(code) {
179
+ return isLang(code) ? code : "en";
180
+ }
154
181
  /**
155
182
  * The language the person reads, for a wizard translated on request: the first of their
156
183
  * languages (the browser's, in its order), else the main language of the country their time
@@ -192,6 +219,10 @@ export function langNameEn(code) {
192
219
  export function localeOf(code) {
193
220
  return isLang(code) ? LOCALES[code] : code;
194
221
  }
222
+ /** The locale speech is recognised and read aloud in for any language. */
223
+ export function speechLocaleOf(code) {
224
+ return isLang(code) ? SPEECH_LOCALES[code] : code;
225
+ }
195
226
  export const FIELD_KINDS = [
196
227
  "text",
197
228
  "textarea",
@@ -793,9 +824,14 @@ export const wizardSchema = z.object({
793
824
  intro: z.string().optional(),
794
825
  /**
795
826
  * The language the wizard speaks to people: its texts, the runner's words, its numbers and
796
- * dates. Without one it is read from the wizard's own words (`wizardLang`).
827
+ * dates, by its two-letter code ("en", "de", "it"). Any language, not only those engenty's own
828
+ * strings are written in; `validateWizard` checks it names a real one. Without one it is read
829
+ * from the wizard's own words (`wizardLang`).
797
830
  */
798
- lang: z.enum(LANGS).optional(),
831
+ lang: z
832
+ .string()
833
+ .regex(/^[a-z]{2}$/)
834
+ .optional(),
799
835
  /** Tabular data the wizard keeps between runs, per person — or for all runs (`shared`). */
800
836
  lists: z.array(listSchema).max(12).optional(),
801
837
  /** Accounts the person connects; kept between runs, per person. */
@@ -875,6 +911,11 @@ export function validateWizard(def, files) {
875
911
  const sharedLists = new Set();
876
912
  const connectionIds = new Set();
877
913
  const notShown = (id) => `List "${id}" is shared: every run writes it, so the person running the wizard never sees it.`;
914
+ if (def.lang && !isRealLanguage(def.lang)) {
915
+ issues.push({
916
+ message: `"${def.lang}" isn't a language code. Use a two-letter code such as "en", "de" or "it".`,
917
+ });
918
+ }
878
919
  for (const list of def.lists ?? []) {
879
920
  if (listIds.has(list.id)) {
880
921
  issues.push({ message: `Duplicate list id "${list.id}".` });
@@ -1566,17 +1607,6 @@ function isEmptyValue(v) {
1566
1607
  export function conditionsOf(when) {
1567
1608
  return when === undefined ? [] : Array.isArray(when) ? when : [when];
1568
1609
  }
1569
- /** A typed number as a number: "89,90" from a German number pad counts as 89.9. */
1570
- function numberOf(v) {
1571
- if (typeof v === "number") {
1572
- return Number.isFinite(v) ? v : null;
1573
- }
1574
- if (typeof v !== "string" || !v.trim()) {
1575
- return null;
1576
- }
1577
- const n = Number(v.replace(/\s/g, "").replace(",", "."));
1578
- return Number.isFinite(n) ? n : null;
1579
- }
1580
1610
  export function conditionMatches(c, values) {
1581
1611
  const v = values[c.field];
1582
1612
  switch (c.op) {
@@ -1592,8 +1622,8 @@ export function conditionMatches(c, values) {
1592
1622
  return Array.isArray(c.value) && c.value.map(String).includes(String(v));
1593
1623
  case "gt":
1594
1624
  case "lt": {
1595
- const a = numberOf(v);
1596
- const b = numberOf(c.value);
1625
+ const a = readNumber(v);
1626
+ const b = readNumber(c.value);
1597
1627
  return a !== null && b !== null && (c.op === "gt" ? a > b : a < b);
1598
1628
  }
1599
1629
  case "contains":
@@ -1769,7 +1799,7 @@ export function slotDayText(iso, lang = "en") {
1769
1799
  if (!slot) {
1770
1800
  return iso;
1771
1801
  }
1772
- const locale = LOCALES[lang];
1802
+ const locale = localeOf(lang);
1773
1803
  const weekday = slot.wall.toLocaleDateString(locale, { weekday: "long", timeZone: "UTC" });
1774
1804
  const date = slot.wall.toLocaleDateString(locale, {
1775
1805
  day: "numeric",
@@ -1784,7 +1814,7 @@ export function slotText(iso, lang = "en") {
1784
1814
  if (!slot) {
1785
1815
  return iso;
1786
1816
  }
1787
- const locale = LOCALES[lang];
1817
+ const locale = localeOf(lang);
1788
1818
  const weekday = slot.wall
1789
1819
  .toLocaleDateString(locale, { weekday: "short", timeZone: "UTC" })
1790
1820
  .replace(/\.$/, "");
@@ -1818,8 +1848,8 @@ export function itemsTotals(field, value, values) {
1818
1848
  let vatRate = field.vat?.rate ?? 0;
1819
1849
  if (field.vat?.field) {
1820
1850
  const raw = values[field.vat.field];
1821
- const parsed = Number.parseFloat(String(raw ?? "").replace(",", "."));
1822
- if (Number.isFinite(parsed)) {
1851
+ const parsed = readNumber(raw, { units: true });
1852
+ if (parsed !== null) {
1823
1853
  vatRate = parsed;
1824
1854
  }
1825
1855
  }
@@ -1835,8 +1865,7 @@ export function itemsTotals(field, value, values) {
1835
1865
  }
1836
1866
  /** A typed amount as a number: a phone's number pad in German gives "89,90". */
1837
1867
  function amountOf(v) {
1838
- const n = typeof v === "string" ? Number(v.replace(/\s/g, "").replace(",", ".")) : Number(v);
1839
- return Number.isFinite(n) ? n : 0;
1868
+ return readNumber(v) ?? 0;
1840
1869
  }
1841
1870
  function round2(n) {
1842
1871
  return Math.round(n * 100) / 100;
@@ -20,6 +20,12 @@ export interface FeatureDeclarationInput {
20
20
  }
21
21
  /** A plugin's declarations under its id: `contacts` + `leadScoring` → `contacts.leadScoring`. */
22
22
  export declare function declaredByPlugin(pluginId: string, features: Record<string, FeatureDeclarationInput | boolean> | undefined): Record<string, FeatureDeclaration>;
23
+ /**
24
+ * Whether a switch is on in resolved values: a switch they do not carry counts as declared – a
25
+ * core flag with its default (`ownKeys` on, `teamPlans` off), anything else off. Every read of a
26
+ * flag goes through this (`useFeature` in the studio), so a missing value means one thing.
27
+ */
28
+ export declare function featureOn(features: Features | null | undefined, name: string): boolean;
23
29
  /** The defaults of these declarations; without any, the core's. */
24
30
  export declare function defaultFeatures(declared?: Record<string, FeatureDeclaration>): Features;
25
31
  /**
@@ -25,6 +25,18 @@ export function declaredByPlugin(pluginId, features) {
25
25
  }
26
26
  return out;
27
27
  }
28
+ /**
29
+ * Whether a switch is on in resolved values: a switch they do not carry counts as declared – a
30
+ * core flag with its default (`ownKeys` on, `teamPlans` off), anything else off. Every read of a
31
+ * flag goes through this (`useFeature` in the studio), so a missing value means one thing.
32
+ */
33
+ export function featureOn(features, name) {
34
+ const value = features?.[name];
35
+ if (typeof value === "boolean") {
36
+ return value;
37
+ }
38
+ return Object.hasOwn(FEATURES, name) ? FEATURES[name].default : false;
39
+ }
28
40
  /** The defaults of these declarations; without any, the core's. */
29
41
  export function defaultFeatures(declared = FEATURES) {
30
42
  const out = {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Numbers as people write and read them: what a phone's number pad, a table cell or a document
3
+ * gives, and an amount or a file's size as a page shows it. The server, the studio and the runner read and
4
+ * show them the same way.
5
+ */
6
+ /**
7
+ * A number as a person types it: digits with at most one decimal mark, a point or a comma
8
+ * alike, as a phone's number pad gives it in either language (`1.5`, `1,5`, `89,90`). There are
9
+ * no thousands separators: `1.234` and `1,234` are 1.234, and anything with two marks
10
+ * (`1.234,50`, `1.234.567`) is no number, since only the writer's language could say which mark
11
+ * is which. Text that is not typed (a document, a website) is read by a model into numbers
12
+ * before it gets here. With `units`, what stands around the number is dropped (`38 h`,
13
+ * `19 %`); without, anything but the number makes it none. Null where there is no number.
14
+ */
15
+ export declare function readNumber(value: unknown, opts?: {
16
+ units?: boolean;
17
+ }): number | null;
18
+ /**
19
+ * A file's size for a list, in decimal units as the operating systems show them: `1 KB` for
20
+ * anything below a kilobyte but nothing (`0 KB`), `340 KB`, `2.4 MB`, `1.2 GB`.
21
+ */
22
+ export declare function formatBytes(bytes: number): string;
23
+ /** An amount in a currency, as the language writes it (`1.234,50 €`); an unknown currency code as `1234.50 XYZ`. */
24
+ export declare function formatMoney(amount: number, currency: string, locale: string): string;
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: FSL-1.1-MIT OR MIT
2
+ /**
3
+ * Numbers as people write and read them: what a phone's number pad, a table cell or a document
4
+ * gives, and an amount or a file's size as a page shows it. The server, the studio and the runner read and
5
+ * show them the same way.
6
+ */
7
+ /**
8
+ * A number as a person types it: digits with at most one decimal mark, a point or a comma
9
+ * alike, as a phone's number pad gives it in either language (`1.5`, `1,5`, `89,90`). There are
10
+ * no thousands separators: `1.234` and `1,234` are 1.234, and anything with two marks
11
+ * (`1.234,50`, `1.234.567`) is no number, since only the writer's language could say which mark
12
+ * is which. Text that is not typed (a document, a website) is read by a model into numbers
13
+ * before it gets here. With `units`, what stands around the number is dropped (`38 h`,
14
+ * `19 %`); without, anything but the number makes it none. Null where there is no number.
15
+ */
16
+ export function readNumber(value, opts = {}) {
17
+ if (typeof value === "number") {
18
+ return Number.isFinite(value) ? value : null;
19
+ }
20
+ if (typeof value !== "string") {
21
+ return null;
22
+ }
23
+ const s = opts.units ? value.replace(/[^\d,.-]/g, "") : value.trim();
24
+ if (!/^-?(\d+([.,]\d*)?|[.,]\d+)$/.test(s)) {
25
+ return null;
26
+ }
27
+ const n = Number(s.replace(",", "."));
28
+ return Number.isFinite(n) ? n : null;
29
+ }
30
+ /**
31
+ * A file's size for a list, in decimal units as the operating systems show them: `1 KB` for
32
+ * anything below a kilobyte but nothing (`0 KB`), `340 KB`, `2.4 MB`, `1.2 GB`.
33
+ */
34
+ export function formatBytes(bytes) {
35
+ if (!(bytes > 0)) {
36
+ return "0 KB";
37
+ }
38
+ if (bytes < 999_500) {
39
+ return `${Math.max(1, Math.round(bytes / 1000))} KB`;
40
+ }
41
+ if (bytes < 999_950_000) {
42
+ return `${(bytes / 1_000_000).toFixed(1)} MB`;
43
+ }
44
+ return `${(bytes / 1_000_000_000).toFixed(1)} GB`;
45
+ }
46
+ /** An amount in a currency, as the language writes it (`1.234,50 €`); an unknown currency code as `1234.50 XYZ`. */
47
+ export function formatMoney(amount, currency, locale) {
48
+ try {
49
+ return new Intl.NumberFormat(locale, { style: "currency", currency }).format(amount);
50
+ }
51
+ catch {
52
+ return `${amount.toFixed(2)} ${currency}`;
53
+ }
54
+ }
@@ -501,8 +501,8 @@ export interface RunView {
501
501
  status: RunStatus;
502
502
  mode: "test" | "live";
503
503
  wizard: Pick<WizardDefinition, "title" | "description" | "avatar" | "intro">;
504
- /** The language the wizard speaks: what is read aloud, and in which voice. */
505
- lang: Lang;
504
+ /** The language the wizard speaks, by its code: what is read aloud, and in which voice. */
505
+ lang: string;
506
506
  /**
507
507
  * The run in the viewer's language (a wizard translated on request): every text of the view
508
508
  * is written in it, the choices keep their values and show `labels[value]`. Null: the
@@ -627,8 +627,8 @@ export interface PublicWizard {
627
627
  kind: "page" | "channel";
628
628
  url: string | null;
629
629
  }[];
630
- /** The language the wizard is written in: the page opens in it. */
631
- lang: Lang;
630
+ /** The language the wizard is written in, by its code: the page opens in it. */
631
+ lang: string;
632
632
  /** The author offers the wizard in the viewer's language, translated on request. */
633
633
  translatable: boolean;
634
634
  /** The page in the viewer's language, where it was asked for (`?lang=`); null: the original. */
@@ -108,3 +108,10 @@ export declare function rowKey(def: ListDef, cells: Record<string, unknown>): st
108
108
  export declare function newCells(def: ListDef, input: Record<string, unknown>): Record<string, unknown>;
109
109
  /** An existing row's cells with some columns changed; cells of columns the list no longer has are dropped. */
110
110
  export declare function patchCells(def: ListDef, current: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
111
+ /**
112
+ * What a person typed into a cell, as the value sent for its column; the runtime still checks it
113
+ * against the column. Blank is null; a number or a duration (in the column's input unit) is read
114
+ * as people write it ("12,5"); a date and time from the browser's picker, in local time, becomes
115
+ * ISO. What cannot be read goes as typed, for the runtime to refuse and name.
116
+ */
117
+ export declare function cellValue(column: TableColumn, text: string): unknown;
@@ -1,5 +1,6 @@
1
1
  // SPDX-License-Identifier: FSL-1.1-MIT OR MIT
2
2
  import { coerceRowValues, mergeRowValues } from "./engenty/data-tables/index.js";
3
+ import { readNumber } from "./numbers.js";
3
4
  export const STORE_LIMITS = {
4
5
  fileBytes: 15_000_000,
5
6
  totalBytes: 200_000_000,
@@ -51,3 +52,30 @@ export function patchCells(def, current, patch) {
51
52
  const kept = Object.fromEntries(Object.entries(current).filter(([id]) => known.has(id)));
52
53
  return mergeRowValues(def.columns, kept, patch);
53
54
  }
55
+ /**
56
+ * What a person typed into a cell, as the value sent for its column; the runtime still checks it
57
+ * against the column. Blank is null; a number or a duration (in the column's input unit) is read
58
+ * as people write it ("12,5"); a date and time from the browser's picker, in local time, becomes
59
+ * ISO. What cannot be read goes as typed, for the runtime to refuse and name.
60
+ */
61
+ export function cellValue(column, text) {
62
+ if (text.trim() === "") {
63
+ return null;
64
+ }
65
+ switch (column.type) {
66
+ case "number":
67
+ case "duration":
68
+ return readNumber(text) ?? text;
69
+ case "boolean":
70
+ return text === "true";
71
+ case "date": {
72
+ if (column.format.kind !== "datetime") {
73
+ return text;
74
+ }
75
+ const at = new Date(text);
76
+ return Number.isNaN(at.getTime()) ? text : at.toISOString();
77
+ }
78
+ default:
79
+ return text;
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wizards",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Make a wish. Get your wizard. It guides you step by step. Let AI do the work. Runs on your machine, on the AI subscription you already have.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "homepage": "https://engenty.ai",
@@ -1,191 +0,0 @@
1
- import { posix } from "node:path";
2
- const WORKDIR = "/workspace";
3
- const EXEC_TIMEOUT_MS = 120_000;
4
- const INSTALL_TIMEOUT_MS = 300_000;
5
- /** The only hosts the VM reaches: where npm and Python packages come from. */
6
- const REGISTRIES = ["registry.npmjs.org", "pypi.org", "files.pythonhosted.org"];
7
- /** python at the start of a command, after an operator, or behind VAR=value assignments. */
8
- const PYTHON_IN_SHELL = /(?:^|[;&|(\n`]|\$\()\s*(?:\w+=\S*\s+)*python3?(?:\.\d+)?(?=\s|$)/;
9
- /**
10
- * What an agentOS VM runs: a WASM userland (a POSIX shell and coreutils, no native binaries),
11
- * Node in a V8 isolate and Python as Pyodide. Python started from the shell never returns, so
12
- * it is offered as its own tool; of the compiled wheels only numpy loads reliably.
13
- */
14
- export const AGENTOS_CAPABILITIES = {
15
- commands: [
16
- "sh",
17
- "ls",
18
- "cat",
19
- "head",
20
- "tail",
21
- "sort",
22
- "wc",
23
- "grep",
24
- "sed",
25
- "awk",
26
- "find",
27
- "diff",
28
- "tar",
29
- "gzip",
30
- "base64",
31
- "node",
32
- "npm",
33
- ],
34
- python: "tool",
35
- pythonPackages: "Python 3.13 standard library; `packages` installs numpy and pure-Python packages from PyPI such as openpyxl or pypdf — no pandas, pillow or matplotlib",
36
- network: "none, except the npm and PyPI registries",
37
- uses: "calculations or file conversion",
38
- workdir: WORKDIR,
39
- };
40
- let runtime = null;
41
- /** One native sidecar process hosts the VM of every run; the first sandbox starts it. */
42
- function agentOsRuntime() {
43
- runtime ??= (async () => {
44
- const [core, runtimeCore] = await Promise.all([
45
- import("@rivet-dev/agentos-core"),
46
- import("@rivet-dev/agentos-runtime-core"),
47
- ]).catch((err) => {
48
- throw new Error(`SANDBOX=agentos needs @rivet-dev/agentos-core in this build: ${err.message}`);
49
- });
50
- keepRuntimeCommands(runtimeCore.SidecarProcess);
51
- return { AgentOs: core.AgentOs, sidecar: await core.AgentOs.createSidecar() };
52
- })().catch((err) => {
53
- runtime = null;
54
- throw err;
55
- });
56
- return runtime;
57
- }
58
- const patched = new WeakSet();
59
- /**
60
- * agentOS registers `python` as a kernel command when it creates a VM and leaves it out when
61
- * it configures that VM; naming the runtime commands there again is what makes Python start.
62
- */
63
- function keepRuntimeCommands(SidecarProcess) {
64
- const proto = SidecarProcess.prototype;
65
- if (patched.has(proto)) {
66
- return;
67
- }
68
- patched.add(proto);
69
- const configureVm = proto.configureVm;
70
- proto.configureVm = function (session, vm, options) {
71
- const commands = [
72
- "node",
73
- "npm",
74
- "npx",
75
- "python",
76
- "python3",
77
- ...(options?.bootstrapCommands ?? []),
78
- ];
79
- return configureVm.call(this, session, vm, {
80
- ...options,
81
- bootstrapCommands: [...new Set(commands)],
82
- });
83
- };
84
- }
85
- /** Stops the sidecar process. The next sandbox starts a new one. */
86
- export async function stopAgentOs() {
87
- const current = runtime;
88
- runtime = null;
89
- await (await current?.catch(() => null))?.sidecar.dispose();
90
- }
91
- function toResult(result, python = false) {
92
- const stdout = result.stdout ?? "";
93
- let stderr = result.stderr ?? "";
94
- if (python) {
95
- // A Python traceback arrives with the interpreter's own JavaScript stack behind it.
96
- stderr = stderr.replace(/\n\s+at [\s\S]*$/, "\n");
97
- }
98
- if (result.outcome === "timed_out") {
99
- stderr = `${stderr}\ntimed out`.trim();
100
- }
101
- else if (result.outcome !== "succeeded" && !stdout && !stderr) {
102
- stderr = result.error.message;
103
- }
104
- return { exitCode: result.exitCode ?? (result.outcome === "succeeded" ? 0 : 1), stdout, stderr };
105
- }
106
- /** One agentOS VM per run, inside the server process's sidecar: no Docker, no container. */
107
- export class AgentOsSandbox {
108
- capabilities = AGENTOS_CAPABILITIES;
109
- machine = null;
110
- installed = new Set();
111
- vm() {
112
- this.machine ??= (async () => {
113
- const { AgentOs, sidecar } = await agentOsRuntime();
114
- return AgentOs.create({
115
- sidecar: { kind: "explicit", handle: sidecar },
116
- // Anything not listed is refused, the host's own loopback included.
117
- permissions: {
118
- fs: "allow",
119
- childProcess: "allow",
120
- process: "allow",
121
- env: "allow",
122
- network: {
123
- default: "deny",
124
- rules: [
125
- {
126
- mode: "allow",
127
- operations: ["*"],
128
- patterns: REGISTRIES.flatMap((host) => [`dns://${host}`, `tcp://${host}:443`]),
129
- },
130
- ],
131
- },
132
- },
133
- });
134
- })();
135
- return this.machine;
136
- }
137
- async exec(command, options = {}) {
138
- if (PYTHON_IN_SHELL.test(command)) {
139
- return {
140
- exitCode: 127,
141
- stdout: "",
142
- stderr: "python is not a shell command in this sandbox; run Python with run_python.",
143
- };
144
- }
145
- const vm = await this.vm();
146
- return toResult(await vm.process.exec(command, {
147
- cwd: options.cwd ?? WORKDIR,
148
- env: options.env,
149
- timeoutMs: options.timeoutMs ?? EXEC_TIMEOUT_MS,
150
- output: { capture: "all" },
151
- }));
152
- }
153
- async runPython(source, options = {}) {
154
- const vm = await this.vm();
155
- const missing = (options.packages ?? []).filter((name) => !this.installed.has(name));
156
- if (missing.length) {
157
- const install = toResult(await vm.python.install(missing, {
158
- timeoutMs: INSTALL_TIMEOUT_MS,
159
- output: { capture: "all" },
160
- }));
161
- // A wheel that is listed but not shipped "installs" and then fails to load.
162
- if (install.exitCode !== 0 || /Failed to load/.test(install.stdout)) {
163
- return { ...install, exitCode: install.exitCode || 1 };
164
- }
165
- for (const name of missing) {
166
- this.installed.add(name);
167
- }
168
- }
169
- return toResult(await vm.python.execute(source, {
170
- // Python starts in its HOME whatever cwd says, and only /workspace is shared with the shell.
171
- env: { ...options.env, HOME: WORKDIR },
172
- timeoutMs: options.timeoutMs ?? EXEC_TIMEOUT_MS,
173
- output: { capture: "all" },
174
- }), true);
175
- }
176
- async writeFile(path, data) {
177
- const vm = await this.vm();
178
- const target = posix.resolve(WORKDIR, path);
179
- await vm.filesystem.mkdir(posix.dirname(target), { recursive: true });
180
- await vm.filesystem.writeFile(target, data);
181
- }
182
- async readFile(path) {
183
- const vm = await this.vm();
184
- return vm.filesystem.readFile(posix.resolve(WORKDIR, path));
185
- }
186
- async destroy() {
187
- const machine = this.machine;
188
- this.machine = null;
189
- await (await machine?.catch(() => null))?.dispose();
190
- }
191
- }