wizards 0.6.2 → 0.7.1

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 (88) 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/cli/autostart.js +58 -12
  5. package/apps/runtime/dist/cli/autoupdate.js +221 -0
  6. package/apps/runtime/dist/cli/connect.js +6 -2
  7. package/apps/runtime/dist/cli/environment.js +5 -0
  8. package/apps/runtime/dist/cli/machine.js +7 -3
  9. package/apps/runtime/dist/cli/main.js +10 -3
  10. package/apps/runtime/dist/cli/menu.js +27 -8
  11. package/apps/runtime/dist/cli/processes.js +72 -0
  12. package/apps/runtime/dist/cli/setup.js +26 -26
  13. package/apps/runtime/dist/cli/start.js +18 -1
  14. package/apps/runtime/dist/cli/status.js +51 -17
  15. package/apps/runtime/dist/cli/uninstall-main.js +20 -0
  16. package/apps/runtime/dist/cli/uninstall.js +231 -0
  17. package/apps/runtime/dist/cli/update.js +25 -2
  18. package/apps/runtime/dist/cli/versions.js +106 -0
  19. package/apps/runtime/dist/docs/user/command-line.md +3 -2
  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/steps.js +3 -3
  24. package/apps/runtime/dist/engine/template.js +8 -13
  25. package/apps/runtime/dist/env.js +3 -20
  26. package/apps/runtime/dist/features.js +2 -2
  27. package/apps/runtime/dist/index.js +3 -1
  28. package/apps/runtime/dist/manage.js +7 -0
  29. package/apps/runtime/dist/models.js +12 -0
  30. package/apps/runtime/dist/sandbox/docker.js +0 -2
  31. package/apps/runtime/dist/sandbox/index.js +3 -10
  32. package/apps/runtime/dist/services/space-categories.js +3 -25
  33. package/apps/runtime/dist/tools/browser.js +2 -14
  34. package/apps/runtime/dist/tools/index.js +1 -19
  35. package/apps/runtime/dist/tools/mail.js +1 -4
  36. package/apps/runtime/dist/tools/shared.js +30 -3
  37. package/apps/runtime/dist/update.js +10 -19
  38. package/apps/web/dist/assets/AboutDialog-BJALish1.js +1 -0
  39. package/apps/web/dist/assets/{ChangelogDialog-BRiSYyIn.js → ChangelogDialog-CxmHNvq1.js} +1 -1
  40. package/apps/web/dist/assets/ChatView-C9ASPVJj.js +10 -0
  41. package/apps/web/dist/assets/EditorPage-BHhNEiKa.js +3 -0
  42. package/apps/web/dist/assets/{FlowPreview-CUOEzoeJ.js → FlowPreview-DvpjuQdC.js} +1 -1
  43. package/apps/web/dist/assets/{Harness-D3kTVsF5.js → Harness-Duq0wpfg.js} +2 -2
  44. package/apps/web/dist/assets/{NewWizardChannels-C3PGe_Zd.js → NewWizardChannels-CGFy67jk.js} +1 -1
  45. package/apps/web/dist/assets/NewWizardPage-DwBcRWJj.js +7 -0
  46. package/apps/web/dist/assets/{OnboardingPage-pOPB5TVZ.js → OnboardingPage-YwKzupPI.js} +1 -1
  47. package/apps/web/dist/assets/ProjectPage-DP1xMjbe.js +1 -0
  48. package/apps/web/dist/assets/PublicRunner-CECAuP0g.js +1 -0
  49. package/apps/web/dist/assets/{ReadOnly-CCxZvYrT.js → ReadOnly-BnnGcLWU.js} +1 -1
  50. package/apps/web/dist/assets/RunDrawer-C0vf8QAc.js +4 -0
  51. package/apps/web/dist/assets/{RunnerView-pKik3sdE.js → RunnerView-CBrZpjt5.js} +1 -1
  52. package/apps/web/dist/assets/SettingsPage-C0-mVcs5.js +1 -0
  53. package/apps/web/dist/assets/SetupPage-BGlYQREU.js +1 -0
  54. package/apps/web/dist/assets/{SharePage-CZQqp5JM.js → SharePage-IwxlV6WC.js} +1 -1
  55. package/apps/web/dist/assets/changelog-CPNPseoQ.js +1 -0
  56. package/apps/web/dist/assets/{esm-D4pSRQgT.js → esm-6TNBQNCD.js} +1 -1
  57. package/apps/web/dist/assets/index-DU-Onx-c.js +14 -0
  58. package/apps/web/dist/assets/{lock-DYnHCyRg.js → lock-HLZib0u9.js} +1 -1
  59. package/apps/web/dist/assets/{phone-DJawY1RF.js → phone-DsxbIcWi.js} +1 -1
  60. package/apps/web/dist/assets/settings-menu-BMMkIVP1.js +1 -0
  61. package/apps/web/dist/assets/ui-Brg2qM1A.js +137 -0
  62. package/apps/web/dist/assets/{upload-DL6NRa4h.js → upload-BaGy0ppG.js} +1 -1
  63. package/apps/web/dist/index.html +2 -2
  64. package/apps/web/dist/install.sh +30 -12
  65. package/apps/web/dist/mcp/flow.html +11 -11
  66. package/bin/wizards.mjs +17 -3
  67. package/node_modules/@engenty-wizards/shared/dist/definition.js +6 -17
  68. package/node_modules/@engenty-wizards/shared/dist/features.d.ts +6 -0
  69. package/node_modules/@engenty-wizards/shared/dist/features.js +12 -0
  70. package/node_modules/@engenty-wizards/shared/dist/numbers.d.ts +24 -0
  71. package/node_modules/@engenty-wizards/shared/dist/numbers.js +54 -0
  72. package/node_modules/@engenty-wizards/shared/dist/store.d.ts +7 -0
  73. package/node_modules/@engenty-wizards/shared/dist/store.js +28 -0
  74. package/package.json +1 -1
  75. package/apps/runtime/dist/sandbox/agentos.js +0 -191
  76. package/apps/web/dist/assets/AboutDialog-BrfbtjmR.js +0 -1
  77. package/apps/web/dist/assets/ChatView-_dYvuuUY.js +0 -10
  78. package/apps/web/dist/assets/EditorPage-CkRjTRMg.js +0 -3
  79. package/apps/web/dist/assets/NewWizardPage-B3ol4K43.js +0 -7
  80. package/apps/web/dist/assets/ProjectPage-Boyptzz_.js +0 -1
  81. package/apps/web/dist/assets/PublicRunner-DcETj9_E.js +0 -1
  82. package/apps/web/dist/assets/RunDrawer-hwruVTDG.js +0 -4
  83. package/apps/web/dist/assets/SettingsPage-BOe6NgFg.js +0 -1
  84. package/apps/web/dist/assets/SetupPage-81QxPnfE.js +0 -1
  85. package/apps/web/dist/assets/changelog-CtyrKHAc.js +0 -1
  86. package/apps/web/dist/assets/index-DnIvQFIg.js +0 -14
  87. package/apps/web/dist/assets/settings-menu-B_-_EAmK.js +0 -1
  88. package/apps/web/dist/assets/ui-rh8TaWvT.js +0 -137
@@ -0,0 +1,231 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, lstatSync, readdirSync, readlinkSync, rmdirSync, rmSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, resolve, sep } from "node:path";
5
+ import * as p from "@clack/prompts";
6
+ import { LOGIN_LABEL, present, removeUnit, UNIT, unloadLoginItem } from "./autostart.js";
7
+ import { apps as allApps } from "./connect.js";
8
+ import { findWizardsProcesses, stopProcesses } from "./processes.js";
9
+ import { settings } from "./start.js";
10
+ import { bad, cyan, dim, no, ok } from "./ui.js";
11
+ export const realSystem = () => ({
12
+ userHome: homedir(),
13
+ platform: process.platform,
14
+ env: process.env,
15
+ interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
16
+ apps: allApps,
17
+ processes: findWizardsProcesses,
18
+ stop: (pids) => stopProcesses(pids),
19
+ unloadLoginItem,
20
+ removeUnit,
21
+ });
22
+ /** Parts of the install folder that are the person's: kept or deleted with the data. */
23
+ const WITH_DATA = [".env", ".env.local", "plugins"];
24
+ function loginItemPath(system) {
25
+ if (system.platform === "darwin") {
26
+ return join(system.userHome, "Library", "LaunchAgents", `${LOGIN_LABEL}.plist`);
27
+ }
28
+ if (system.platform === "linux") {
29
+ const config = system.env.XDG_CONFIG_HOME || join(system.userHome, ".config");
30
+ return join(config, "systemd", "user", UNIT);
31
+ }
32
+ return null;
33
+ }
34
+ /** The links install.sh put into ~/.local/bin that point into the install. */
35
+ function ourLinks(home, system) {
36
+ return ["wizards", "engenty-wizards"]
37
+ .map((name) => join(system.userHome, ".local", "bin", name))
38
+ .filter((link) => {
39
+ try {
40
+ return (lstatSync(link).isSymbolicLink() &&
41
+ resolve(dirname(link), readlinkSync(link)).startsWith(home + sep));
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ });
47
+ }
48
+ /** Works out what an uninstall takes away, without changing anything. */
49
+ export async function makePlan(paths, system) {
50
+ const apps = [];
51
+ for (const app of system.apps(paths)) {
52
+ if (await app.connected().catch(() => false)) {
53
+ apps.push(app);
54
+ }
55
+ }
56
+ const item = loginItemPath(system);
57
+ let loginItem = null;
58
+ try {
59
+ loginItem = item && present(item) ? item : null;
60
+ }
61
+ catch {
62
+ // Can't look: count it as there, so the uninstall tries and says so.
63
+ loginItem = item;
64
+ }
65
+ const { dataDir } = settings(paths, system.env);
66
+ const program = [];
67
+ const data = existsSync(dataDir) ? [dataDir] : [];
68
+ for (const name of existsSync(paths.home) ? readdirSync(paths.home) : []) {
69
+ const path = join(paths.home, name);
70
+ if (path === dataDir) {
71
+ continue;
72
+ }
73
+ const empty = name === "plugins" && readdirSync(path).length === 0;
74
+ (WITH_DATA.includes(name) && !empty ? data : program).push(path);
75
+ }
76
+ return {
77
+ home: paths.home,
78
+ apps,
79
+ loginItem,
80
+ processes: system.processes(paths.home),
81
+ program,
82
+ links: ourLinks(paths.home, system),
83
+ data,
84
+ };
85
+ }
86
+ /** Size of a folder as `du` counts it, e.g. " (805 MB)"; "" where it can't tell. */
87
+ function size(path) {
88
+ const kb = Number(spawnSync("du", ["-sk", path], { encoding: "utf8" }).stdout?.split(/\s/)[0]);
89
+ return kb > 0 ? ` (${kb < 1024 ? `${kb} KB` : `${Math.round(kb / 1024)} MB`})` : "";
90
+ }
91
+ const processes = (n) => `${n} running process${n === 1 ? "" : "es"}`;
92
+ /** `wizards uninstall [--dry-run] [--yes] [--delete-data]`. */
93
+ export async function uninstall(paths, options, system = realSystem()) {
94
+ const t = (path) => path.startsWith(system.userHome + sep) ? `~${path.slice(system.userHome.length)}` : path;
95
+ if (!options.dryRun && !options.yes && !system.interactive) {
96
+ console.error("The uninstall asks before it deletes anything, and there is no terminal to ask in.\n" +
97
+ "Run it with --yes (your data is kept), --yes --delete-data, or --dry-run to only look.");
98
+ return 2;
99
+ }
100
+ const plan = await makePlan(paths, system);
101
+ const steps = [
102
+ ...plan.apps.map((app) => `Disconnect ${app.name}`),
103
+ ...(plan.loginItem ? [`Remove the login item ${t(plan.loginItem)}`] : []),
104
+ ...(plan.processes.length ? [`Stop ${processes(plan.processes.length)}`] : []),
105
+ ...(plan.program.length ? [`Delete the program in ${t(plan.home)}${size(plan.home)}`] : []),
106
+ ...plan.links.map((link) => `Delete the command ${t(link)}`),
107
+ ];
108
+ if (!steps.length && !plan.data.length) {
109
+ console.log(`${ok} engenty wizards isn’t installed here.`);
110
+ return 0;
111
+ }
112
+ if (steps.length) {
113
+ console.log("\nThis removes engenty wizards:");
114
+ }
115
+ for (const step of steps) {
116
+ console.log(` ${no} ${step}`);
117
+ }
118
+ if (plan.data.length) {
119
+ console.log(`\nYour data: ${plan.data.map((path) => t(path) + size(path)).join(", ")}`);
120
+ console.log(dim(" Kept unless you choose to delete it. A reinstall picks it up again."));
121
+ }
122
+ console.log("");
123
+ if (options.dryRun) {
124
+ console.log(dim("Nothing was changed (--dry-run)."));
125
+ return 0;
126
+ }
127
+ let deleteData = options.yes && options.deleteData;
128
+ if (!options.yes) {
129
+ if (plan.data.length) {
130
+ const answer = await p.confirm({
131
+ message: "Delete your data too? Your wizards, runs and results are gone for good.",
132
+ initialValue: false,
133
+ });
134
+ if (p.isCancel(answer)) {
135
+ return 130;
136
+ }
137
+ deleteData = answer;
138
+ }
139
+ const go = await p.confirm({
140
+ message: deleteData ? "Uninstall and delete your data now?" : "Uninstall now?",
141
+ });
142
+ if (p.isCancel(go) || !go) {
143
+ p.cancel("Nothing was changed.");
144
+ return 130;
145
+ }
146
+ }
147
+ const failed = (what, error) => console.log(`${bad} ${what}: ${error.message.split("\n")[0]}`);
148
+ for (const app of plan.apps) {
149
+ const result = await app
150
+ .disconnect()
151
+ .catch((error) => ({ ok: false, message: error.message }));
152
+ console.log(result.ok ? `${ok} Disconnected ${app.name}` : `${bad} ${app.name}: ${result.message}`);
153
+ }
154
+ if (plan.loginItem) {
155
+ try {
156
+ if (system.platform === "darwin") {
157
+ await system.unloadLoginItem();
158
+ rmSync(plan.loginItem, { force: true });
159
+ }
160
+ else {
161
+ await system.removeUnit(plan.loginItem);
162
+ }
163
+ console.log(`${ok} Removed the login item`);
164
+ }
165
+ catch (error) {
166
+ failed("The login item", error);
167
+ }
168
+ }
169
+ // Twice: an AI app's `wizards mcp` may start the studio again meanwhile.
170
+ for (let round = 0; round < 2; round++) {
171
+ const running = system.processes(paths.home);
172
+ if (running.length && !(await system.stop(running.map((proc) => proc.pid))).length) {
173
+ console.log(`${ok} Stopped ${processes(running.length)}`);
174
+ }
175
+ }
176
+ for (const path of [...plan.program, ...plan.links, ...(deleteData ? plan.data : [])]) {
177
+ try {
178
+ rmSync(path, { recursive: true, force: true });
179
+ }
180
+ catch (error) {
181
+ failed(`Couldn’t delete ${t(path)}`, error);
182
+ }
183
+ }
184
+ for (const dir of [plan.home, join(system.userHome, ".engenty")]) {
185
+ try {
186
+ rmdirSync(dir);
187
+ }
188
+ catch {
189
+ // not empty, or not there
190
+ }
191
+ }
192
+ // A second look: what is still there.
193
+ const after = await makePlan(paths, system);
194
+ const left = [
195
+ ...after.apps.map((app) => `${app.name} still lists the wizards: ${cyan(`npx wizards disconnect ${app.id}`)}`),
196
+ ...(after.loginItem ? [t(after.loginItem)] : []),
197
+ ...after.processes.map((proc) => `process ${proc.pid}: ${cyan(`kill -9 ${proc.pid}`)}`),
198
+ ...[...after.program, ...after.links, ...(deleteData ? after.data : [])].map(t),
199
+ ];
200
+ if (!deleteData && after.data.length) {
201
+ console.log(`${ok} Kept your data: ${after.data.map(t).join(", ")}`);
202
+ }
203
+ if (left.length) {
204
+ console.log(`\n${bad} Still there:`);
205
+ for (const line of left) {
206
+ console.log(` ${line}`);
207
+ }
208
+ return 1;
209
+ }
210
+ console.log(`\n${ok} engenty wizards is uninstalled.\n`);
211
+ return 0;
212
+ }
213
+ /** The options of `uninstall` from its command line; a message when something is off. */
214
+ export function parseUninstall(argv) {
215
+ const options = { dryRun: false, yes: false, deleteData: false };
216
+ for (const arg of argv) {
217
+ if (arg === "--dry-run") {
218
+ options.dryRun = true;
219
+ }
220
+ else if (arg === "--yes" || arg === "-y") {
221
+ options.yes = true;
222
+ }
223
+ else if (arg === "--delete-data") {
224
+ options.deleteData = true;
225
+ }
226
+ else if (arg !== "uninstall") {
227
+ return `Unknown option ${arg}. The uninstall takes --dry-run, --yes and --delete-data.`;
228
+ }
229
+ }
230
+ return options;
231
+ }
@@ -1,10 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { installedByScript, isCheckout } from "./home.js";
4
+ import { autoUpdatePolicy, noteUpdated } from "./autoupdate.js";
5
+ import { installedByScript, isCheckout, readInstallNote, writeInstallNote, } from "./home.js";
5
6
  import { runningRuntime, settings } from "./start.js";
6
7
  import { restart } from "./status.js";
7
- import { cmd, cyan } from "./ui.js";
8
+ import { cmd, cyan, no, ok } from "./ui.js";
9
+ import { currentVersion } from "./versions.js";
8
10
  /** The installer this install was made with; it also updates one. */
9
11
  const INSTALLER = "https://engenty.ai/install.sh";
10
12
  async function installer(into) {
@@ -34,6 +36,7 @@ export async function update(paths, options = {}) {
34
36
  : `This copy runs from npm. To update: ${cyan("npx wizards@latest")}`);
35
37
  return 0;
36
38
  }
39
+ const before = currentVersion(paths);
37
40
  const tmp = mkdtempSync(join(paths.home, "update-"));
38
41
  try {
39
42
  const script = await installer(tmp);
@@ -53,6 +56,10 @@ export async function update(paths, options = {}) {
53
56
  finally {
54
57
  rmSync(tmp, { recursive: true, force: true });
55
58
  }
59
+ const after = currentVersion(paths);
60
+ if (before && after) {
61
+ noteUpdated(paths, before, after);
62
+ }
56
63
  if (await runningRuntime(settings(paths).dataDir)) {
57
64
  if (options.restart) {
58
65
  return restart(paths);
@@ -61,3 +68,19 @@ export async function update(paths, options = {}) {
61
68
  }
62
69
  return 0;
63
70
  }
71
+ /** `wizards autoupdate [on|off]`: whether updates come by themselves (autoupdate.ts). */
72
+ export function autoUpdateCommand(paths, value) {
73
+ if (value && value !== "on" && value !== "off") {
74
+ console.error(`Use ${cmd("autoupdate on")} or ${cmd("autoupdate off")}.`);
75
+ return 2;
76
+ }
77
+ const installed = installedByScript(paths);
78
+ if (value && installed) {
79
+ writeInstallNote({ ...readInstallNote(paths), autoUpdate: value === "on" }, paths);
80
+ }
81
+ const policy = autoUpdatePolicy(paths, { installed });
82
+ console.log(policy.on
83
+ ? `${ok} Updates come by themselves: downloaded in the background, used from the next start.`
84
+ : `${no} Updates don’t come by themselves: ${policy.reason}. To update now: ${cmd("update")}`);
85
+ return value && !installed ? 1 : 0;
86
+ }
@@ -0,0 +1,106 @@
1
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ /**
4
+ * The runtimes of an install made by install.sh, one folder per version, side by side:
5
+ *
6
+ * runtime/<version>/ an npm prefix: node_modules/wizards, package.json, package-lock.json
7
+ * runtime/current → <version>, the one `bin/wizards` starts
8
+ * runtime/node_modules → current/node_modules, for commands written before the versions
9
+ *
10
+ * Up to 0.6.2 `runtime/` itself was the prefix; `switchTo` moves such an install into its
11
+ * version folder first. A process keeps the files of the version it started with.
12
+ */
13
+ const VERSION = /^\d+\.\d+\.\d+$/;
14
+ export const runtimeDir = (paths, version) => join(paths.runtime, version);
15
+ const currentLink = (paths) => join(paths.runtime, "current");
16
+ const modules = (paths) => join(paths.runtime, "node_modules");
17
+ /** The version of the package `wizards` in an npm prefix; null when there is none. */
18
+ export function versionIn(prefix) {
19
+ try {
20
+ const file = join(prefix, "node_modules", "wizards", "package.json");
21
+ const { version } = JSON.parse(readFileSync(file, "utf8"));
22
+ return typeof version === "string" ? version : null;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ const isLink = (path) => {
29
+ try {
30
+ return lstatSync(path).isSymbolicLink();
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ };
36
+ /** The version `bin/wizards` starts; null without a runtime. */
37
+ export function currentVersion(paths) {
38
+ if (isLink(currentLink(paths))) {
39
+ return versionIn(join(paths.runtime, readlinkSync(currentLink(paths))));
40
+ }
41
+ return versionIn(paths.runtime);
42
+ }
43
+ /** The version folders there are, each with a complete package in it. */
44
+ export function installedVersions(paths) {
45
+ try {
46
+ return readdirSync(paths.runtime)
47
+ .filter((name) => VERSION.test(name) && versionIn(join(paths.runtime, name)) === name)
48
+ .sort();
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ /** Points `link` at `target` in one step: a new link renamed over the old one. */
55
+ function link(target, path) {
56
+ const tmp = `${path}.${process.pid}.tmp`;
57
+ rmSync(tmp, { force: true });
58
+ symlinkSync(target, tmp);
59
+ renameSync(tmp, path);
60
+ }
61
+ /** Moves a runtime from before the version folders into `runtime/<version>/`. */
62
+ function adoptLegacy(paths) {
63
+ const version = versionIn(paths.runtime);
64
+ if (isLink(modules(paths)) || !version) {
65
+ return;
66
+ }
67
+ const into = runtimeDir(paths, version);
68
+ rmSync(into, { recursive: true, force: true });
69
+ mkdirSync(into, { recursive: true });
70
+ for (const name of ["node_modules", "package.json", "package-lock.json"]) {
71
+ if (existsSync(join(paths.runtime, name))) {
72
+ renameSync(join(paths.runtime, name), join(into, name));
73
+ }
74
+ }
75
+ link(version, currentLink(paths));
76
+ }
77
+ /** Makes `version` the one `bin/wizards` starts from now on. */
78
+ export function switchTo(paths, version) {
79
+ if (versionIn(runtimeDir(paths, version)) !== version) {
80
+ throw new Error(`${runtimeDir(paths, version)} holds no complete runtime ${version}.`);
81
+ }
82
+ adoptLegacy(paths);
83
+ link(version, currentLink(paths));
84
+ link(join("current", "node_modules"), modules(paths));
85
+ }
86
+ /** Deletes the version folders other than `keep` and the current one; returns what went. */
87
+ export function prune(paths, keep) {
88
+ const current = currentVersion(paths);
89
+ const stale = installedVersions(paths).filter((v) => v !== current && !keep.includes(v));
90
+ for (const version of stale) {
91
+ rmSync(runtimeDir(paths, version), { recursive: true, force: true });
92
+ }
93
+ return stale;
94
+ }
95
+ /** Puts an npm prefix installed elsewhere into its version folder (never the current one). */
96
+ export function promote(paths, staged, version) {
97
+ if (versionIn(staged) !== version) {
98
+ throw new Error(`The download holds version ${versionIn(staged) ?? "none"}, not ${version}.`);
99
+ }
100
+ if (currentVersion(paths) === version) {
101
+ rmSync(staged, { recursive: true, force: true });
102
+ return;
103
+ }
104
+ rmSync(runtimeDir(paths, version), { recursive: true, force: true });
105
+ renameSync(staged, runtimeDir(paths, version));
106
+ }
@@ -8,7 +8,7 @@ version: 0.5.0
8
8
 
9
9
  | Command | What it does |
10
10
  |---|---|
11
- | `wizards` | Shows what is running, and a menu. While it runs, you can open the studio, restart or stop it; while it doesn’t, “Start in the background” or “Start here” (in this terminal). Then “Update”, which restarts it into the new version, “Start at login”, “Connect AI apps”, “Account”, “Status”, “Setup” and “Help”. The first time, the setup runs first. It needs a terminal |
11
+ | `wizards` | Shows what is running, and a menu. While it runs, you can open the studio, restart or stop it; while it doesn’t, “Start in the background” or “Start in this terminal”. Then “Update”, which restarts it into the new version, “Start at login”, “Connect AI apps”, “Account”, “Status”, “Setup”, “Uninstall” and “Help”. The first time, the setup runs first. It needs a terminal |
12
12
  | `wizards start` | Starts it and opens the studio. It runs in that terminal, and Ctrl-C stops it. The first time, the setup runs first |
13
13
  | `wizards studio` | Opens the studio in your browser. If it isn’t running, it starts in the background first |
14
14
  | `wizards setup` | Runs the guided setup again: your account, an AI client, the browser and ffmpeg, start at login, your AI apps. `--local` leaves the account out ([Local only](./local-only.md)) |
@@ -18,13 +18,14 @@ version: 0.5.0
18
18
  | `wizards doctor` | Shows the status and checks for problems |
19
19
  | `wizards logs [n]` | Prints the last lines engenty wizards printed while running (100, or `n` up to 2000). Keys and tokens are taken out |
20
20
  | `wizards report` | Sends the status, the model settings (never a key) and the last 500 log lines to your [account](./models.md#account), so support can see what went wrong. Nothing is sent unless you run it. The command names the report’s ID. If this computer isn’t signed in, it tells you to sign in under Settings → Account or with `wizards account sign-in` |
21
- | `wizards stop` | Stops it. Your data is kept |
21
+ | `wizards stop` | Stops it, with any other copy of it still running in the background. Your data is kept. AI apps that use your wizards start it again when you use one there |
22
22
  | `wizards restart` | Stops it and starts it again; after an update, that is the new version. If it isn’t running, it starts in the background |
23
23
  | `wizards update` | Installs the latest version |
24
24
  | `wizards autostart on` / `off` | Starts it when you log in, or stops doing so. It is a LaunchAgent on macOS and a systemd user service on Linux |
25
25
  | `wizards connect [app …]` | Adds your wizards to your [AI apps](./ai-apps.md): every app found, or the ones you name |
26
26
  | `wizards disconnect [app …]` | Takes them out again |
27
27
  | `wizards mcp` | Runs the MCP server over stdio. AI apps start it; you don’t |
28
+ | `wizards uninstall` | Takes engenty wizards off this computer. It shows what it will remove first and asks about your data on its own; your data is kept unless you choose to delete it. `--dry-run` only shows the plan, `--yes` asks nothing and keeps your data, `--yes --delete-data` deletes it too. See [Remove](./install.md#remove) |
28
29
  | `wizards validate <path …>` | Checks wizards without the studio: `.wizard` files, unpacked wizard files, or a wizard’s JSON. `--json` prints the problems as JSON. A space’s connectors and plugins are checked when you import it |
29
30
 
30
31
  | Option | What it does |
@@ -27,7 +27,7 @@ Keychain (as `engenty-wizards`). On other systems they are stored encrypted in t
27
27
  |---|---|
28
28
  | Back up | Stop engenty wizards (`wizards stop`) and copy the folder |
29
29
  | Move to another computer | Install engenty wizards there, stop it and put the folder in place. On a Mac, enter your API keys and sign in to your account again: the Keychain doesn’t move |
30
- | Start over | Delete the folder. On a Mac, the keys in the Keychain stay and are used again. To remove them, delete the `engenty-wizards` entries in Keychain Access |
30
+ | Start over | Run `wizards uninstall`, choose to delete your data, then install again. On a Mac, the keys in the Keychain stay and are used again. To remove them, delete the `engenty-wizards` entries in Keychain Access |
31
31
  | Keep it elsewhere | Set `ENGENTY_HOME` to another folder. engenty wizards then keeps everything in its `wizards` folder there |
32
32
 
33
33
  After an update, engenty wizards brings your data up to date the next time it starts.
@@ -71,6 +71,19 @@ the right of the address bar.
71
71
 
72
72
  ## Update
73
73
 
74
+ engenty wizards updates itself: it looks for a new version when it starts and once a day,
75
+ downloads it in the background and checks it, and switches to it at the next start (a restart or
76
+ the next login), never while a wizard runs. The version before stays beside it. `wizards status`
77
+ and the studio then say “Updated to …” with what changed. To turn it off:
78
+
79
+ ```bash
80
+ wizards autoupdate off
81
+ ```
82
+
83
+ or set `WIZARDS_DISABLE_AUTOUPDATE=1` (in the terminal or in `~/.engenty/wizards/.env`). In CI,
84
+ and for an install set up without a terminal, it is off unless you turn it on with `wizards
85
+ autoupdate on`. `npx wizards` never updates itself. To update now:
86
+
74
87
  ```bash
75
88
  wizards update
76
89
  ```
@@ -88,11 +101,21 @@ brought up to date at the next start.
88
101
  ## Remove
89
102
 
90
103
  ```bash
91
- wizards stop
92
- wizards autostart off
93
- wizards disconnect
104
+ wizards uninstall
94
105
  ```
95
106
 
96
- `wizards disconnect` takes your wizards out of your AI apps again. Then delete `~/.engenty/wizards`
97
- and `~/.local/bin/wizards`. Your wizards and results are in `~/.engenty/wizards/data`:
98
- [back that folder up](./data.md) first if you want to keep them.
107
+ First it shows what it will remove: your wizards in your AI apps, starting at login, everything of
108
+ engenty wizards that is running, the program in `~/.engenty/wizards` and the command
109
+ `~/.local/bin/wizards`. Your data in `~/.engenty/wizards/data` is kept unless you say to delete
110
+ it, and installing again picks it up. [Back it up](./data.md) first if you delete it. At the end
111
+ it lists anything it couldn’t remove.
112
+
113
+ If the `wizards` command no longer runs, `npx wizards uninstall` does the same.
114
+
115
+ | Option | What it does |
116
+ |---|---|
117
+ | `--dry-run` | Only shows what it would remove |
118
+ | `--yes` | Removes everything without asking and keeps your data |
119
+ | `--yes --delete-data` | Deletes your data as well |
120
+
121
+ Without a terminal, it asks for `--yes`.
@@ -1,4 +1,5 @@
1
1
  import { isDecidedField, isLang, isLocationValue, MAX_FILES, offeredSlot, shownFields, } from "@engenty-wizards/shared/definition";
2
+ import { readNumber } from "@engenty-wizards/shared/numbers";
2
3
  import { note, saidText } from "@engenty-wizards/shared/run";
3
4
  /** The field errors in a language, for whoever reads them. */
4
5
  export function fieldErrorsIn(errors, lang) {
@@ -13,11 +14,10 @@ function coerce(field, raw) {
13
14
  }
14
15
  switch (field.kind) {
15
16
  case "number": {
16
- if (raw === "") {
17
+ if (String(raw).trim() === "") {
17
18
  return undefined;
18
19
  }
19
- const n = Number(String(raw).replace(",", "."));
20
- return Number.isFinite(n) ? n : Number.NaN;
20
+ return readNumber(raw) ?? Number.NaN;
21
21
  }
22
22
  case "toggle":
23
23
  return raw === true || raw === "true" || raw === "on";
@@ -62,8 +62,7 @@ function coerce(field, raw) {
62
62
  if (c.kind === "text") {
63
63
  return [c.id, String(v ?? "").slice(0, 2000)];
64
64
  }
65
- const n = Number(String(v ?? "").replace(",", "."));
66
- return [c.id, Number.isFinite(n) ? n : 0];
65
+ return [c.id, readNumber(v) ?? 0];
67
66
  })))
68
67
  .filter((r) => Object.values(r).some((v) => v !== "" && v !== 0))
69
68
  .slice(0, 200);
@@ -11,7 +11,7 @@ import { snapshotHtmlImages } from "../files/web-images.js";
11
11
  import { runFilmStep } from "../film/step.js";
12
12
  import { generateImageMedia, generateSpeechMedia, generateVideoMedia, } from "../media/generate.js";
13
13
  import { markMedia } from "../media/marking.js";
14
- import { attachTools, costOf, isHarnessVendor, textModel } from "../models.js";
14
+ import { agentModel, attachTools, costOf, isHarnessVendor, textModel, totalUsage, } from "../models.js";
15
15
  import { knowledgeBlock } from "../services/knowledge.js";
16
16
  import { buildStepTools } from "../tools/index.js";
17
17
  import { callPluginTool, spaceContextOf } from "../tools/plugin.js";
@@ -536,7 +536,7 @@ export async function runAgentStep(step, ctx) {
536
536
  id: `step-${step.id}`,
537
537
  name: step.title,
538
538
  instructions: system,
539
- model: resolved.model,
539
+ model: agentModel(resolved),
540
540
  tools,
541
541
  });
542
542
  // Working through a mailbox or a row of portals takes many small tool calls.
@@ -564,7 +564,7 @@ export async function runAgentStep(step, ctx) {
564
564
  const result = step.output.format === "json"
565
565
  ? await streamWithProgress(agent, message, { maxSteps, abortSignal: ctx.signal }, step, ctx)
566
566
  : await agent.generate(message, { maxSteps, abortSignal: ctx.signal });
567
- await ctx.chargeUsd(costOf(resolved, result.totalUsage ?? result.usage));
567
+ await ctx.chargeUsd(costOf(resolved, await totalUsage(result)));
568
568
  // `result.text` strings together what the model said between tool calls ("I open the page …").
569
569
  // A written result is its last message; structuring keeps everything, numbers may sit anywhere.
570
570
  const closing = String(result.steps?.at?.(-1)?.text ?? "").trim();
@@ -1,15 +1,8 @@
1
1
  import { allFields, isAudioValue, isLocationValue, itemsTotals, localeOf, locationText, wizardLang, } from "@engenty-wizards/shared/definition";
2
+ import { formatMoney } from "@engenty-wizards/shared/numbers";
2
3
  import { fontLicenseWords, gradientCss, textStyleLine, } from "@engenty-wizards/shared/projects";
3
4
  import { listAsMarkdown } from "@engenty-wizards/shared/store";
4
5
  const TEMPLATE_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
5
- function money(n, currency, locale) {
6
- try {
7
- return new Intl.NumberFormat(locale, { style: "currency", currency }).format(n);
8
- }
9
- catch {
10
- return `${n.toFixed(2)} ${currency}`;
11
- }
12
- }
13
6
  /** Line items as a Markdown table with their totals — what a model needs to write a document. */
14
7
  export function itemsAsMarkdown(field, value, values, lang = "en") {
15
8
  const locale = localeOf(lang);
@@ -21,13 +14,15 @@ export function itemsAsMarkdown(field, value, values, lang = "en") {
21
14
  const head = `| ${cols.map((c) => c.label).join(" | ")} | Amount |\n|${cols.map(() => " --- ").join("|")}| ---: |`;
22
15
  const body = totals.rows.map((r) => `| ${cols
23
16
  .map((c) => c.kind === "money"
24
- ? money(Number(r[c.id]) || 0, totals.currency, locale)
17
+ ? formatMoney(Number(r[c.id]) || 0, totals.currency, locale)
25
18
  : String(r[c.id] ?? ""))
26
- .join(" | ")} | ${money(r.amount, totals.currency, locale)} |`);
19
+ .join(" | ")} | ${formatMoney(r.amount, totals.currency, locale)} |`);
27
20
  const sums = [
28
- `Net: ${money(totals.net, totals.currency, locale)}`,
29
- totals.vatRate ? `VAT ${totals.vatRate}%: ${money(totals.vat, totals.currency, locale)}` : null,
30
- `Total: ${money(totals.gross, totals.currency, locale)}`,
21
+ `Net: ${formatMoney(totals.net, totals.currency, locale)}`,
22
+ totals.vatRate
23
+ ? `VAT ${totals.vatRate}%: ${formatMoney(totals.vat, totals.currency, locale)}`
24
+ : null,
25
+ `Total: ${formatMoney(totals.gross, totals.currency, locale)}`,
31
26
  ].filter(Boolean);
32
27
  return [head, ...body, "", ...sums].join("\n");
33
28
  }
@@ -23,8 +23,7 @@ function loadEnvFiles() {
23
23
  loadEnvFiles();
24
24
  const str = (key, fallback = "") => process.env[key]?.trim() || fallback;
25
25
  /**
26
- * Without `SANDBOX`: Docker when the sandbox image is on this machine, else agentOS when its
27
- * packages are installed (a checkout has them, the image and the desktop app do not), else none.
26
+ * Without `SANDBOX`: Docker when the sandbox image is on this machine, else none.
28
27
  * The image is not published, so a fresh install would otherwise offer a tool that cannot start.
29
28
  */
30
29
  function sandboxEngine(image) {
@@ -33,12 +32,9 @@ function sandboxEngine(image) {
33
32
  return "off";
34
33
  }
35
34
  if (engine) {
36
- return engine === "agentos" ? "agentos" : "docker";
37
- }
38
- if (dockerHasImage(image)) {
39
35
  return "docker";
40
36
  }
41
- return agentOsInstalled() ? "agentos" : "off";
37
+ return dockerHasImage(image) ? "docker" : "off";
42
38
  }
43
39
  function dockerHasImage(image) {
44
40
  try {
@@ -49,19 +45,6 @@ function dockerHasImage(image) {
49
45
  return false;
50
46
  }
51
47
  }
52
- /** agentOS ships its sidecar for macOS and Linux on arm64 and x64 only. */
53
- function agentOsInstalled() {
54
- if (!["darwin", "linux"].includes(process.platform) || !["arm64", "x64"].includes(process.arch)) {
55
- return false;
56
- }
57
- try {
58
- import.meta.resolve("@rivet-dev/agentos-core");
59
- return true;
60
- }
61
- catch {
62
- return false;
63
- }
64
- }
65
48
  /** A comma-separated list. */
66
49
  const list = (key) => str(key)
67
50
  .split(",")
@@ -234,7 +217,7 @@ export const env = {
234
217
  : "/usr/bin/chromium")),
235
218
  /** Encodes widget animations to MP4; without it widgets offer no video. */
236
219
  ffmpegPath: str("FFMPEG_PATH", "ffmpeg"),
237
- /** What runs an agent's shell and code: a Docker container, an agentOS VM, or nothing. */
220
+ /** What runs an agent's shell and code: a Docker container or nothing. */
238
221
  sandbox: sandboxEngine(sandboxImage),
239
222
  sandboxImage,
240
223
  /** Assets and workspace files in an S3-compatible bucket (R2); empty = the data folder. */
@@ -1,6 +1,6 @@
1
1
  import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
- import { declaredByPlugin, defaultFeatures, FEATURES, parseFeatures, resolveFeatures, } from "@engenty-wizards/shared/features";
3
+ import { declaredByPlugin, defaultFeatures, FEATURES, featureOn, parseFeatures, resolveFeatures, } from "@engenty-wizards/shared/features";
4
4
  import { accountOverview } from "./auth/account.js";
5
5
  import { isCheckout, layout, packageRoot } from "./cli/home.js";
6
6
  import { env } from "./env.js";
@@ -61,7 +61,7 @@ export function features(remote) {
61
61
  return resolveFeatures(defaultFeatures(declaredFeatures()), envFeatures(), remote, fileFeatures());
62
62
  }
63
63
  export function feature(name, remote) {
64
- return features(remote)[name] === true;
64
+ return featureOn(features(remote), name);
65
65
  }
66
66
  let accountCache = null;
67
67
  /**
@@ -17,7 +17,7 @@ import { purgeExpiredRuns } from "./services/shares.js";
17
17
  import { purgeUnusedStores } from "./store/index.js";
18
18
  import { forgetRunner, registerRunner } from "./tenants/control.js";
19
19
  import { LOCAL_TENANT } from "./tenants/tenant.js";
20
- import { onRestart, RESTART_EXIT } from "./update.js";
20
+ import { onRestart, RESTART_EXIT, startUpdateChecks } from "./update.js";
21
21
  // Before anything prints: the last lines stay in memory for a look from afar (debug.ts).
22
22
  hookConsole();
23
23
  await migrateControlDb();
@@ -39,6 +39,8 @@ else {
39
39
  void loadCatalog();
40
40
  void checkModelsAtStart().catch(() => undefined);
41
41
  startProChecks();
42
+ // A newer release is downloaded beside this one and used from the next start.
43
+ startUpdateChecks();
42
44
  }
43
45
  // The marketplace's starters and the starred entries, kept for offline use; checked hourly.
44
46
  void syncMarketplace();