wizards 0.1.11 → 0.2.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 (113) hide show
  1. package/README.md +3 -0
  2. package/apps/runtime/dist/agents/architect.js +6 -4
  3. package/apps/runtime/dist/app.js +38 -4
  4. package/apps/runtime/dist/auth/local-ticket.js +8 -5
  5. package/apps/runtime/dist/authoring/guide.js +10 -2
  6. package/apps/runtime/dist/cli/connect.js +531 -0
  7. package/apps/runtime/dist/cli/environment.js +8 -1
  8. package/apps/runtime/dist/cli/home.js +1 -0
  9. package/apps/runtime/dist/cli/main.js +17 -0
  10. package/apps/runtime/dist/cli/mcp.js +175 -0
  11. package/apps/runtime/dist/cli/setup.js +49 -0
  12. package/apps/runtime/dist/db/client.js +13 -0
  13. package/apps/runtime/dist/engine/runner.js +3 -0
  14. package/apps/runtime/dist/env.js +18 -4
  15. package/apps/runtime/dist/index.js +3 -0
  16. package/apps/runtime/dist/mcp/auth.js +26 -0
  17. package/apps/runtime/dist/mcp/flow-app.js +94 -0
  18. package/apps/runtime/dist/mcp/handler.js +15 -5
  19. package/apps/runtime/dist/mcp/seen.js +65 -0
  20. package/apps/runtime/dist/mcp/tools.js +158 -5
  21. package/apps/runtime/dist/plugins/discovery.js +144 -0
  22. package/apps/runtime/dist/plugins/events.js +40 -0
  23. package/apps/runtime/dist/plugins/loader.js +277 -0
  24. package/apps/runtime/dist/plugins/migrations.js +69 -0
  25. package/apps/runtime/dist/plugins/registry.js +105 -0
  26. package/apps/runtime/dist/routes/plugins.js +160 -0
  27. package/apps/runtime/dist/routes/runs.js +7 -3
  28. package/apps/runtime/dist/routes/studio.js +20 -0
  29. package/apps/runtime/dist/secrets/signing.js +21 -3
  30. package/apps/runtime/dist/services/runs.js +190 -28
  31. package/apps/runtime/dist/services/wizards.js +30 -4
  32. package/apps/runtime/dist/tools/index.js +2 -0
  33. package/apps/runtime/dist/tools/plugin.js +43 -0
  34. package/apps/web/dist/assets/ChangelogDialog-Co_9fs61.js +1 -0
  35. package/apps/web/dist/assets/ChatPanel-Br3nYkUc.js +1 -0
  36. package/apps/web/dist/assets/CreditsDialog-D3fZRqXH.js +1 -0
  37. package/apps/web/dist/assets/EditorPage-BSjQC6FE.js +2 -0
  38. package/apps/web/dist/assets/FlowPreview-CjUK5046.js +1 -0
  39. package/apps/web/dist/assets/HomePage-8v4HqIbP.js +1 -0
  40. package/apps/web/dist/assets/ImportWizard-DiV62Uno.js +1 -0
  41. package/apps/web/dist/assets/{LocalRuntime-DKqEzptp.js → LocalRuntime-CTyLGC98.js} +2 -2
  42. package/apps/web/dist/assets/NewWizardPage-BBL2yenw.js +2 -0
  43. package/apps/web/dist/assets/PublicRunner-BwtoEVlE.js +1 -0
  44. package/apps/web/dist/assets/RunnerView-DOizlTad.js +2 -0
  45. package/apps/web/dist/assets/SettingsPage-CBDv04YS.js +2 -0
  46. package/apps/web/dist/assets/SetupPage-CfXTOaIm.js +1 -0
  47. package/apps/web/dist/assets/SharePage-CgByqrAF.js +1 -0
  48. package/apps/web/dist/assets/api-KMi35npD.js +10 -0
  49. package/apps/web/dist/assets/{arrow-left-UpjOBfQp.js → arrow-left-hTN1VoH1.js} +1 -1
  50. package/apps/web/dist/assets/{arrow-right-CucQycEy.js → arrow-right-6FyN3e7D.js} +1 -1
  51. package/apps/web/dist/assets/changelog-03JgixQA.js +1 -0
  52. package/apps/web/dist/assets/check-CUpby_J9.js +1 -0
  53. package/apps/web/dist/assets/{createLucideIcon-DBu2Ovxj.js → createLucideIcon-q3MZMmnf.js} +1 -1
  54. package/apps/web/dist/assets/definition-DndaH5Ow.js +55 -0
  55. package/apps/web/dist/assets/engenty-Cm8yZLON.js +1 -0
  56. package/apps/web/dist/assets/esm-rJY59Ixx.js +7 -0
  57. package/apps/web/dist/assets/{external-link-CRg6DVdO.js → external-link-MAL7vJYj.js} +1 -1
  58. package/apps/web/dist/assets/{fluffy-engenty-CJ3wCQgT.js → fluffy-engenty-D7rFrNcJ.js} +2 -2
  59. package/apps/web/dist/assets/index-B9YVjvVY.css +1 -0
  60. package/apps/web/dist/assets/index-BmpnE7IO.js +10 -0
  61. package/apps/web/dist/assets/jsx-runtime-D3jfb0Ew.js +1 -0
  62. package/apps/web/dist/assets/meta-4R1Lnr25.js +1 -0
  63. package/apps/web/dist/assets/{mic-CeqQ89Dj.js → mic-Bvz4mA2j.js} +1 -1
  64. package/apps/web/dist/assets/oss-credits-roB62QJu.js +1 -0
  65. package/apps/web/dist/assets/{outputs-CHe6KRLc.js → outputs-COE_nvAw.js} +2 -2
  66. package/apps/web/dist/assets/plug-CLjyCoHB.js +1 -0
  67. package/apps/web/dist/assets/{plus-HA210wXA.js → plus-Du-Jx8aI.js} +1 -1
  68. package/apps/web/dist/assets/react-dom-DenslgJm.js +1 -0
  69. package/apps/web/dist/assets/session-B6-6VomQ.js +1 -0
  70. package/apps/web/dist/assets/type-Bj9H3eOK.js +1 -0
  71. package/apps/web/dist/assets/ui-Cs6lpsmj.js +1 -0
  72. package/apps/web/dist/assets/{upload-DsgLfQ3U.js → upload--J5bZhEM.js} +1 -1
  73. package/apps/web/dist/assets/{video-Dk1fuTDl.js → video-CjgmvRDk.js} +1 -1
  74. package/apps/web/dist/assets/{wand-sparkles-Bf97fs8T.js → wand-sparkles-CKxArWPY.js} +1 -1
  75. package/apps/web/dist/index.html +10 -7
  76. package/apps/web/dist/mcp/flow.html +748 -0
  77. package/modules/README.md +15 -0
  78. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +144 -0
  79. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.js +4 -0
  80. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +80 -0
  81. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.js +4 -0
  82. package/node_modules/@engenty-wizards/plugin-sdk/package.json +10 -0
  83. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +18 -6
  84. package/node_modules/@engenty-wizards/shared/dist/definition.js +22 -1
  85. package/package.json +6 -1
  86. package/plugin/.claude-plugin/plugin.json +1 -1
  87. package/plugin/skills/use-wizard/SKILL.md +18 -0
  88. package/apps/web/dist/assets/ChangelogDialog-DURK75KD.js +0 -1
  89. package/apps/web/dist/assets/ChatPanel-DZhZJZCP.js +0 -1
  90. package/apps/web/dist/assets/CreditsDialog-CLUOVBLG.js +0 -1
  91. package/apps/web/dist/assets/EditorPage-PGCEQdiC.js +0 -2
  92. package/apps/web/dist/assets/FlowPreview-CqJUNW13.js +0 -1
  93. package/apps/web/dist/assets/HomePage-CRkDMNM2.js +0 -1
  94. package/apps/web/dist/assets/ImportWizard-DX1YtFPv.js +0 -1
  95. package/apps/web/dist/assets/NewWizardPage-BPJrYi2m.js +0 -2
  96. package/apps/web/dist/assets/PublicRunner-B3T244Ry.js +0 -1
  97. package/apps/web/dist/assets/RunnerView-DgYaOHIH.js +0 -2
  98. package/apps/web/dist/assets/SettingsPage-LBVqEwXs.js +0 -2
  99. package/apps/web/dist/assets/SetupPage-DAyJs-hK.js +0 -1
  100. package/apps/web/dist/assets/SharePage-oKGavxJt.js +0 -1
  101. package/apps/web/dist/assets/api-DbPn6Dge.js +0 -10
  102. package/apps/web/dist/assets/changelog-BJkYErYt.js +0 -1
  103. package/apps/web/dist/assets/check-Bjx41xbB.js +0 -1
  104. package/apps/web/dist/assets/definition-cy7uvY5L.js +0 -55
  105. package/apps/web/dist/assets/engenty-CWIFpgKq.js +0 -1
  106. package/apps/web/dist/assets/esm-ZHEGGS6s.js +0 -7
  107. package/apps/web/dist/assets/index-BRy5sz4a.js +0 -10
  108. package/apps/web/dist/assets/index-CQ_q9iMO.css +0 -1
  109. package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +0 -1
  110. package/apps/web/dist/assets/meta-2YMHa2hT.js +0 -1
  111. package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +0 -1
  112. package/apps/web/dist/assets/type-BzYgVWKL.js +0 -1
  113. package/apps/web/dist/assets/ui-tO4ZXbfh.js +0 -1
@@ -0,0 +1,531 @@
1
+ import { execFile, execFileSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { delimiter, dirname, join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { installedByScript, packageRoot } from "./home.js";
7
+ import { findOnPath } from "./machine.js";
8
+ import { bad, dim, ok, tilde } from "./ui.js";
9
+ const run = promisify(execFile);
10
+ /**
11
+ * The wizards in the person's own AI apps: `wizards connect` writes this install's MCP
12
+ * server into each app's config, as the command `wizards mcp` (stdio). Every app starts
13
+ * servers that way; the command finds the running runtime itself and signs in with the data
14
+ * folder's secret, so a config holds neither a port nor a key.
15
+ */
16
+ /** The name the server has in every app's config. */
17
+ export const SERVER_NAME = "engenty-wizards";
18
+ /**
19
+ * How an app starts this install's MCP server. An install made by install.sh has a command that
20
+ * stays where it is across updates; any other copy is started by its Node and its script. The
21
+ * studio connects apps too: from the runtime, which is not the command.
22
+ */
23
+ export function serverCommand(paths, script = join(packageRoot, "bin", "wizards.mjs")) {
24
+ const moved = dirname(paths.home) !== join(homedir(), ".engenty");
25
+ const env = moved ? { env: { ENGENTY_HOME: dirname(paths.home) } } : {};
26
+ if (installedByScript(paths)) {
27
+ return { command: join(paths.bin, "wizards"), args: ["mcp"], ...env };
28
+ }
29
+ return { command: process.execPath, args: [script, "mcp"], ...env };
30
+ }
31
+ /** A JSON config with the server set under `at` (e.g. ["mcpServers"]); null when it is no JSON. */
32
+ export function withJsonServer(text, at, entry, name = SERVER_NAME) {
33
+ let root;
34
+ try {
35
+ root = text?.trim() ? JSON.parse(text) : {};
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ if (!root || typeof root !== "object" || Array.isArray(root)) {
41
+ return null;
42
+ }
43
+ let node = root;
44
+ for (const key of at) {
45
+ const next = node[key];
46
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
47
+ node[key] = {};
48
+ }
49
+ node = node[key];
50
+ }
51
+ node[name] = entry;
52
+ return `${JSON.stringify(root, null, 2)}\n`;
53
+ }
54
+ /** The same config without the server; null when it is no JSON. */
55
+ export function withoutJsonServer(text, at, name = SERVER_NAME) {
56
+ let root;
57
+ try {
58
+ root = JSON.parse(text);
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ let node = root;
64
+ for (const key of at) {
65
+ node = node?.[key];
66
+ }
67
+ if (node && typeof node === "object") {
68
+ delete node[name];
69
+ }
70
+ return `${JSON.stringify(root, null, 2)}\n`;
71
+ }
72
+ export function jsonHasServer(text, at, name = SERVER_NAME) {
73
+ try {
74
+ let node = JSON.parse(text);
75
+ for (const key of at) {
76
+ node = node?.[key];
77
+ }
78
+ return Boolean(node && typeof node === "object" && name in node);
79
+ }
80
+ catch {
81
+ return false;
82
+ }
83
+ }
84
+ const tomlString = (value) => JSON.stringify(value);
85
+ /** A TOML config (Codex) without the server's tables `[mcp_servers.<name>]` and below. */
86
+ export function withoutTomlServer(text, name = SERVER_NAME) {
87
+ const lines = text.split("\n");
88
+ const out = [];
89
+ let skipping = false;
90
+ for (const line of lines) {
91
+ const table = line.match(/^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(#.*)?$/)?.[1];
92
+ if (table !== undefined) {
93
+ const key = table.replaceAll('"', "");
94
+ skipping = key === `mcp_servers.${name}` || key.startsWith(`mcp_servers.${name}.`);
95
+ }
96
+ if (!skipping) {
97
+ out.push(line);
98
+ }
99
+ }
100
+ return out.join("\n").replace(/\n{3,}$/, "\n\n");
101
+ }
102
+ export function withTomlServer(text, server, name = SERVER_NAME) {
103
+ const rest = withoutTomlServer(text ?? "", name).trimEnd();
104
+ const table = [
105
+ `[mcp_servers.${name}]`,
106
+ `command = ${tomlString(server.command)}`,
107
+ `args = [${server.args.map(tomlString).join(", ")}]`,
108
+ // A run waits up to 45 s for a step; the default of 60 s leaves room for it.
109
+ "tool_timeout_sec = 120",
110
+ ...(server.env
111
+ ? [
112
+ `env = { ${Object.entries(server.env)
113
+ .map(([key, value]) => `${key} = ${tomlString(value)}`)
114
+ .join(", ")} }`,
115
+ ]
116
+ : []),
117
+ ].join("\n");
118
+ return `${rest ? `${rest}\n\n` : ""}${table}\n`;
119
+ }
120
+ export function tomlHasServer(text, name = SERVER_NAME) {
121
+ return text.split("\n").some((line) => line.trim() === `[mcp_servers.${name}]`);
122
+ }
123
+ /** Goose's YAML: the server's block below `extensions:`, at two spaces. */
124
+ export function withoutYamlExtension(text, name = SERVER_NAME) {
125
+ const lines = text.split("\n");
126
+ const out = [];
127
+ let skipping = false;
128
+ for (const line of lines) {
129
+ if (line === ` ${name}:`) {
130
+ skipping = true;
131
+ continue;
132
+ }
133
+ if (skipping && line.trim() && !line.startsWith(" ")) {
134
+ skipping = false;
135
+ }
136
+ if (!skipping) {
137
+ out.push(line);
138
+ }
139
+ }
140
+ return out.join("\n");
141
+ }
142
+ export function withYamlExtension(text, server, name = SERVER_NAME) {
143
+ const quote = (value) => JSON.stringify(value);
144
+ const block = [
145
+ ` ${name}:`,
146
+ ` name: ${name}`,
147
+ " type: stdio",
148
+ ` cmd: ${quote(server.command)}`,
149
+ ` args: [${server.args.map(quote).join(", ")}]`,
150
+ " enabled: true",
151
+ " timeout: 300",
152
+ ...(server.env
153
+ ? [
154
+ " envs:",
155
+ ...Object.entries(server.env).map(([key, value]) => ` ${key}: ${quote(value)}`),
156
+ ]
157
+ : []),
158
+ ];
159
+ const lines = withoutYamlExtension(text ?? "", name).split("\n");
160
+ const at = lines.findIndex((line) => /^extensions:\s*(\{\s*\})?\s*$/.test(line));
161
+ if (at === -1) {
162
+ const rest = lines.join("\n").trimEnd();
163
+ return `${rest ? `${rest}\n` : ""}extensions:\n${block.join("\n")}\n`;
164
+ }
165
+ lines[at] = "extensions:";
166
+ lines.splice(at + 1, 0, ...block);
167
+ return lines.join("\n");
168
+ }
169
+ export function yamlHasExtension(text, name = SERVER_NAME) {
170
+ return text.split("\n").includes(` ${name}:`);
171
+ }
172
+ const home = homedir();
173
+ const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
174
+ /** A folder below the per-user application data: Library/Application Support, %APPDATA%, ~/.config. */
175
+ function appSupport(...parts) {
176
+ if (process.platform === "darwin") {
177
+ return join(home, "Library", "Application Support", ...parts);
178
+ }
179
+ if (process.platform === "win32") {
180
+ return join(appData, ...parts);
181
+ }
182
+ return join(process.env.XDG_CONFIG_HOME || join(home, ".config"), ...parts);
183
+ }
184
+ /** Claude Desktop's main process, as `ps` (macOS) or `tasklist` (Windows) names it. */
185
+ const CLAUDE_DESKTOP_PROCESS = {
186
+ darwin: /\/Claude\.app\/Contents\/MacOS\/Claude$/m,
187
+ win32: /^"?Claude\.exe"?,/im,
188
+ };
189
+ /** Whether an app's process runs; false where it cannot be told (Linux, no `ps`). */
190
+ export function processRunning(names, platform = process.platform, list = () => platform === "win32"
191
+ ? execFileSync("tasklist", ["/FO", "CSV", "/NH"], { encoding: "utf8" })
192
+ : execFileSync("ps", ["-axo", "comm"], { encoding: "utf8" })) {
193
+ const pattern = names[platform];
194
+ if (!pattern) {
195
+ return false;
196
+ }
197
+ try {
198
+ return pattern.test(list());
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
204
+ const read = (file) => (existsSync(file) ? readFileSync(file, "utf8") : null);
205
+ /** Writes a config, keeping the version before it once next to it. */
206
+ function save(file, text) {
207
+ mkdirSync(dirname(file), { recursive: true });
208
+ if (existsSync(file)) {
209
+ copyFileSync(file, `${file}.before-engenty`);
210
+ }
211
+ writeFileSync(file, text);
212
+ }
213
+ function jsonApp(def) {
214
+ const running = () => def.ownsFileWhileRunning?.()
215
+ ? {
216
+ ok: false,
217
+ message: `${def.name} is running and would overwrite its config. Quit it (Cmd+Q), then run this again.`,
218
+ }
219
+ : null;
220
+ const snippet = (server) => JSON.stringify(def.at.reduceRight((inner, key) => ({ [key]: inner }), {
221
+ [SERVER_NAME]: def.entry(server),
222
+ }), null, 2);
223
+ return {
224
+ id: def.id,
225
+ name: def.name,
226
+ widgets: def.widgets,
227
+ where: def.file,
228
+ installed: () => existsSync(def.marker),
229
+ connected: async () => jsonHasServer(read(def.file) ?? "", def.at),
230
+ async connect(server) {
231
+ const busy = running();
232
+ if (busy) {
233
+ return busy;
234
+ }
235
+ const next = withJsonServer(read(def.file), def.at, def.entry(server));
236
+ if (next === null) {
237
+ return {
238
+ ok: false,
239
+ message: `${def.file} is not plain JSON (comments?); add this to it:`,
240
+ snippet: snippet(server),
241
+ };
242
+ }
243
+ save(def.file, next);
244
+ return { ok: true, message: `added to ${def.file}` };
245
+ },
246
+ async disconnect() {
247
+ const text = read(def.file);
248
+ if (!text || !jsonHasServer(text, def.at)) {
249
+ return { ok: true, message: "was not connected" };
250
+ }
251
+ const busy = running();
252
+ if (busy) {
253
+ return busy;
254
+ }
255
+ const next = withoutJsonServer(text, def.at);
256
+ if (next === null) {
257
+ return {
258
+ ok: false,
259
+ message: `${def.file} is not plain JSON; remove "${SERVER_NAME}" there.`,
260
+ };
261
+ }
262
+ save(def.file, next);
263
+ return { ok: true, message: `removed from ${def.file}` };
264
+ },
265
+ };
266
+ }
267
+ const stdioEntry = (server) => ({
268
+ command: server.command,
269
+ args: server.args,
270
+ ...(server.env ? { env: server.env } : {}),
271
+ });
272
+ /** A command of an app: on the PATH, or among the clients this install brought. */
273
+ function appCommand(paths, name) {
274
+ return findOnPath(name, [join(paths.clients, "bin"), process.env.PATH ?? ""].join(delimiter));
275
+ }
276
+ function claudeCode(paths) {
277
+ const file = join(home, ".claude.json");
278
+ return {
279
+ id: "claude-code",
280
+ name: "Claude Code",
281
+ widgets: false,
282
+ where: "claude mcp (user scope)",
283
+ installed: () => Boolean(appCommand(paths, "claude")),
284
+ connected: async () => jsonHasServer(read(file) ?? "", ["mcpServers"]),
285
+ async connect(server) {
286
+ const claude = appCommand(paths, "claude");
287
+ if (!claude) {
288
+ return { ok: false, message: "the claude command was not found" };
289
+ }
290
+ // Claude Code keeps rewriting its own file: its command changes it, not we.
291
+ await run(claude, ["mcp", "remove", "--scope", "user", SERVER_NAME]).catch(() => undefined);
292
+ try {
293
+ await run(claude, [
294
+ "mcp",
295
+ "add",
296
+ "--scope",
297
+ "user",
298
+ ...Object.entries(server.env ?? {}).flatMap(([key, value]) => ["-e", `${key}=${value}`]),
299
+ SERVER_NAME,
300
+ "--",
301
+ server.command,
302
+ ...server.args,
303
+ ]);
304
+ return { ok: true, message: "added with `claude mcp add --scope user`" };
305
+ }
306
+ catch (error) {
307
+ return { ok: false, message: error.message.split("\n")[0] };
308
+ }
309
+ },
310
+ async disconnect() {
311
+ const claude = appCommand(paths, "claude");
312
+ if (!claude) {
313
+ return { ok: true, message: "was not connected" };
314
+ }
315
+ await run(claude, ["mcp", "remove", "--scope", "user", SERVER_NAME]).catch(() => undefined);
316
+ return { ok: true, message: "removed with `claude mcp remove`" };
317
+ },
318
+ };
319
+ }
320
+ function codex() {
321
+ const dir = process.env.CODEX_HOME || join(home, ".codex");
322
+ const file = join(dir, "config.toml");
323
+ return {
324
+ id: "codex",
325
+ name: "Codex · ChatGPT app",
326
+ widgets: false,
327
+ where: file,
328
+ installed: () => existsSync(dir),
329
+ connected: async () => tomlHasServer(read(file) ?? ""),
330
+ async connect(server) {
331
+ save(file, withTomlServer(read(file), server));
332
+ return { ok: true, message: `added to ${file}` };
333
+ },
334
+ async disconnect() {
335
+ const text = read(file);
336
+ if (!text || !tomlHasServer(text)) {
337
+ return { ok: true, message: "was not connected" };
338
+ }
339
+ save(file, withoutTomlServer(text));
340
+ return { ok: true, message: `removed from ${file}` };
341
+ },
342
+ };
343
+ }
344
+ function goose() {
345
+ const dir = process.platform === "win32"
346
+ ? join(appData, "Block", "goose", "config")
347
+ : join(process.env.XDG_CONFIG_HOME || join(home, ".config"), "goose");
348
+ const file = join(dir, "config.yaml");
349
+ return {
350
+ id: "goose",
351
+ name: "Goose",
352
+ widgets: true,
353
+ where: file,
354
+ installed: () => existsSync(dir),
355
+ connected: async () => yamlHasExtension(read(file) ?? ""),
356
+ async connect(server) {
357
+ save(file, withYamlExtension(read(file), server));
358
+ return { ok: true, message: `added to ${file}` };
359
+ },
360
+ async disconnect() {
361
+ const text = read(file);
362
+ if (!text || !yamlHasExtension(text)) {
363
+ return { ok: true, message: "was not connected" };
364
+ }
365
+ save(file, withoutYamlExtension(text));
366
+ return { ok: true, message: `removed from ${file}` };
367
+ },
368
+ };
369
+ }
370
+ function openClaw(paths) {
371
+ const dir = join(home, ".openclaw");
372
+ const file = join(dir, "openclaw.json");
373
+ const json = jsonApp({
374
+ id: "openclaw",
375
+ name: "OpenClaw",
376
+ widgets: false,
377
+ file,
378
+ marker: dir,
379
+ at: ["mcp", "servers"],
380
+ entry: stdioEntry,
381
+ });
382
+ return {
383
+ ...json,
384
+ // Its config is JSON5: its own command writes it when it is there.
385
+ async connect(server) {
386
+ const openclaw = appCommand(paths, "openclaw");
387
+ if (!openclaw) {
388
+ return json.connect(server);
389
+ }
390
+ try {
391
+ await run(openclaw, [
392
+ "mcp",
393
+ "set",
394
+ SERVER_NAME,
395
+ "--command",
396
+ server.command,
397
+ "--args",
398
+ ...server.args,
399
+ ]);
400
+ return { ok: true, message: "added with `openclaw mcp set`" };
401
+ }
402
+ catch (error) {
403
+ return { ok: false, message: error.message.split("\n")[0] };
404
+ }
405
+ },
406
+ async disconnect() {
407
+ const openclaw = appCommand(paths, "openclaw");
408
+ if (!openclaw) {
409
+ return json.disconnect();
410
+ }
411
+ await run(openclaw, ["mcp", "unset", SERVER_NAME]).catch(() => undefined);
412
+ return { ok: true, message: "removed with `openclaw mcp unset`" };
413
+ },
414
+ };
415
+ }
416
+ /** The AI apps this install can connect to, in the order they are offered. */
417
+ export function apps(paths) {
418
+ const vscodeUser = appSupport("Code", "User");
419
+ return [
420
+ jsonApp({
421
+ id: "claude-desktop",
422
+ name: "Claude Desktop",
423
+ widgets: true,
424
+ file: join(appSupport("Claude"), "claude_desktop_config.json"),
425
+ marker: appSupport("Claude"),
426
+ at: ["mcpServers"],
427
+ entry: stdioEntry,
428
+ ownsFileWhileRunning: () => processRunning(CLAUDE_DESKTOP_PROCESS),
429
+ }),
430
+ claudeCode(paths),
431
+ codex(),
432
+ jsonApp({
433
+ id: "cursor",
434
+ name: "Cursor",
435
+ widgets: true,
436
+ file: join(home, ".cursor", "mcp.json"),
437
+ marker: join(home, ".cursor"),
438
+ at: ["mcpServers"],
439
+ entry: stdioEntry,
440
+ }),
441
+ jsonApp({
442
+ id: "vscode",
443
+ name: "VS Code (Copilot)",
444
+ widgets: true,
445
+ file: join(vscodeUser, "mcp.json"),
446
+ marker: vscodeUser,
447
+ at: ["servers"],
448
+ entry: (server) => ({ type: "stdio", ...stdioEntry(server) }),
449
+ }),
450
+ jsonApp({
451
+ id: "windsurf",
452
+ name: "Windsurf",
453
+ widgets: false,
454
+ file: join(home, ".codeium", "windsurf", "mcp_config.json"),
455
+ marker: join(home, ".codeium", "windsurf"),
456
+ at: ["mcpServers"],
457
+ entry: stdioEntry,
458
+ }),
459
+ jsonApp({
460
+ id: "gemini",
461
+ name: "Gemini CLI",
462
+ widgets: false,
463
+ file: join(home, ".gemini", "settings.json"),
464
+ marker: join(home, ".gemini"),
465
+ at: ["mcpServers"],
466
+ entry: (server) => ({ ...stdioEntry(server), timeout: 120_000 }),
467
+ }),
468
+ goose(),
469
+ openClaw(paths),
470
+ jsonApp({
471
+ id: "lm-studio",
472
+ name: "LM Studio",
473
+ widgets: false,
474
+ file: join(home, ".lmstudio", "mcp.json"),
475
+ marker: join(home, ".lmstudio"),
476
+ at: ["mcpServers"],
477
+ entry: stdioEntry,
478
+ }),
479
+ ];
480
+ }
481
+ export async function appStates(paths) {
482
+ return Promise.all(apps(paths).map(async (app) => ({
483
+ id: app.id,
484
+ name: app.name,
485
+ widgets: app.widgets,
486
+ where: app.where,
487
+ installed: app.installed(),
488
+ connected: await app.connected(),
489
+ })));
490
+ }
491
+ /** `wizards connect [app …]` and `disconnect`: what was done, app by app. */
492
+ export async function connectCommand(paths, ids, remove) {
493
+ const unknown = ids.filter((id) => !apps(paths).some((app) => app.id === id));
494
+ if (unknown.length) {
495
+ console.error(`Unknown app ${unknown.join(", ")}. One of: ${apps(paths)
496
+ .map((app) => app.id)
497
+ .join(", ")}`);
498
+ return 2;
499
+ }
500
+ const results = await connectApps(paths, ids, remove);
501
+ if (!results.length) {
502
+ console.log("No AI app found on this machine. Name one: connect cursor");
503
+ return 0;
504
+ }
505
+ for (const { app, outcome } of results) {
506
+ console.log(`${outcome.ok ? ok : bad} ${app.name.padEnd(22)} ${dim(tilde(outcome.message))}`);
507
+ if (!outcome.ok && outcome.snippet) {
508
+ console.log(outcome.snippet);
509
+ }
510
+ }
511
+ if (!remove && results.some((r) => r.outcome.ok)) {
512
+ console.log("");
513
+ console.log(dim("Restart the app (or reload its MCP servers) to see the wizards in it."));
514
+ console.log(dim("Claude Desktop, Cursor, VS Code and Goose show a run as a widget; the others as text."));
515
+ }
516
+ return results.every((r) => r.outcome.ok) ? 0 : 1;
517
+ }
518
+ /** Connects (or disconnects) the named apps; without names every app found on this machine. */
519
+ export async function connectApps(paths, ids, remove = false) {
520
+ const all = apps(paths);
521
+ const picked = ids.length
522
+ ? all.filter((app) => ids.includes(app.id))
523
+ : all.filter((app) => app.installed());
524
+ const server = serverCommand(paths);
525
+ const out = [];
526
+ for (const app of picked) {
527
+ const outcome = await (remove ? app.disconnect() : app.connect(server)).catch((error) => ({ ok: false, message: error.message }));
528
+ out.push({ app, outcome });
529
+ }
530
+ return out;
531
+ }
@@ -1,4 +1,4 @@
1
- import { delimiter, dirname, join } from "node:path";
1
+ import { basename, delimiter, dirname, join } from "node:path";
2
2
  /**
3
3
  * Variables of the terminal the runtime inherits. Everything else it gets comes from the
4
4
  * install's `.env` or is set by the command line. A key or a database address that the shell
@@ -35,6 +35,8 @@ const INHERITED = new Set([
35
35
  "SANDBOX",
36
36
  "SANDBOX_ENABLED",
37
37
  "SANDBOX_IMAGE",
38
+ "PLUGINS_DIR",
39
+ "PLUGINS_WATCH",
38
40
  "ACCOUNT_URL",
39
41
  "ACCOUNT_GATEWAY_URL",
40
42
  "CLOUD_URL",
@@ -67,7 +69,10 @@ export function runtimePath(paths, env, node = process.execPath) {
67
69
  */
68
70
  export function runtimeEnv(paths, start, env = process.env, fileEnv = {}, platform = process.platform) {
69
71
  const secretsGiven = env.SECRETS !== undefined || fileEnv.SECRETS !== undefined;
72
+ const pluginsGiven = env.PLUGINS_DIR !== undefined || fileEnv.PLUGINS_DIR !== undefined;
70
73
  return {
74
+ // Plugins live beside the data, in the install's own folder.
75
+ ...(pluginsGiven ? {} : { PLUGINS_DIR: paths.plugins }),
71
76
  ...inherited(env),
72
77
  PATH: runtimePath(paths, env),
73
78
  NODE_ENV: "production",
@@ -75,6 +80,8 @@ export function runtimeEnv(paths, start, env = process.env, fileEnv = {}, platfo
75
80
  APP_URL: start.url,
76
81
  DATA_DIR: start.dataDir,
77
82
  LOCAL_ACCESS_KEY: start.accessKey,
83
+ // The install the runtime belongs to: the studio connects AI apps to its command.
84
+ ...(basename(paths.home) === "wizards" ? { ENGENTY_HOME: dirname(paths.home) } : {}),
78
85
  // Keys entered in the settings live in the Keychain, as with the desktop app.
79
86
  ...(platform === "darwin" && !secretsGiven ? { SECRETS: "keychain" } : {}),
80
87
  };
@@ -21,6 +21,7 @@ export function layout(home = wizardsHome()) {
21
21
  tools: join(home, "tools"),
22
22
  clients: join(home, "clients"),
23
23
  bin: join(home, "bin"),
24
+ plugins: join(home, "plugins"),
24
25
  };
25
26
  }
26
27
  /** The folder this code was installed to: it holds `apps/`, `plugin/` and package.json. */
@@ -1,5 +1,7 @@
1
+ import { apps, connectCommand } from "./connect.js";
1
2
  import { layout, packageVersion, readInstallNote } from "./home.js";
2
3
  import { CLIENTS, nodeIsCurrent } from "./machine.js";
4
+ import { mcp } from "./mcp.js";
3
5
  import { command, setup } from "./setup.js";
4
6
  import { open, start } from "./start.js";
5
7
  import { autostart, status, stop } from "./status.js";
@@ -14,6 +16,12 @@ const HELP = `engenty wizards ${packageVersion()}
14
16
  ${command()} stop stop it; the data is kept
15
17
  ${command()} update the newest version
16
18
  ${command()} autostart start it at login: on, off (macOS: a LaunchAgent; Linux: a systemd service)
19
+ ${command()} connect your wizards in your AI apps: every app found, or name them —
20
+ ${apps(layout())
21
+ .map((app) => app.id)
22
+ .join(", ")}
23
+ ${command()} disconnect take them out again (same names)
24
+ ${command()} mcp the MCP server over stdio, as the AI apps start it
17
25
 
18
26
  --yes, -y ask nothing and install nothing optional
19
27
  --client <name> install this AI client without asking: ${CLIENTS.map((c) => c.id).join(", ")}
@@ -26,6 +34,7 @@ function parse(argv) {
26
34
  const args = {
27
35
  command: "",
28
36
  value: "",
37
+ apps: [],
29
38
  yes: false,
30
39
  open: true,
31
40
  print: false,
@@ -61,6 +70,9 @@ function parse(argv) {
61
70
  else if (args.command === "autostart" && !args.value) {
62
71
  args.value = arg;
63
72
  }
73
+ else if (args.command === "connect" || args.command === "disconnect") {
74
+ args.apps.push(arg);
75
+ }
64
76
  else if (args.command) {
65
77
  return `Unexpected argument ${arg}.`;
66
78
  }
@@ -113,6 +125,11 @@ async function main(argv) {
113
125
  return update(paths);
114
126
  case "autostart":
115
127
  return autostart(paths, args.value);
128
+ case "connect":
129
+ case "disconnect":
130
+ return connectCommand(paths, args.apps, args.command === "disconnect");
131
+ case "mcp":
132
+ return mcp(paths);
116
133
  default:
117
134
  console.error(`Unknown command ${args.command}.\n\n${HELP}`);
118
135
  return 2;