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.
- package/README.md +3 -0
- package/apps/runtime/dist/agents/architect.js +6 -4
- package/apps/runtime/dist/app.js +38 -4
- package/apps/runtime/dist/auth/local-ticket.js +8 -5
- package/apps/runtime/dist/authoring/guide.js +10 -2
- package/apps/runtime/dist/cli/connect.js +531 -0
- package/apps/runtime/dist/cli/environment.js +8 -1
- package/apps/runtime/dist/cli/home.js +1 -0
- package/apps/runtime/dist/cli/main.js +17 -0
- package/apps/runtime/dist/cli/mcp.js +175 -0
- package/apps/runtime/dist/cli/setup.js +49 -0
- package/apps/runtime/dist/db/client.js +13 -0
- package/apps/runtime/dist/engine/runner.js +3 -0
- package/apps/runtime/dist/env.js +18 -4
- package/apps/runtime/dist/index.js +3 -0
- package/apps/runtime/dist/mcp/auth.js +26 -0
- package/apps/runtime/dist/mcp/flow-app.js +94 -0
- package/apps/runtime/dist/mcp/handler.js +15 -5
- package/apps/runtime/dist/mcp/seen.js +65 -0
- package/apps/runtime/dist/mcp/tools.js +158 -5
- package/apps/runtime/dist/plugins/discovery.js +144 -0
- package/apps/runtime/dist/plugins/events.js +40 -0
- package/apps/runtime/dist/plugins/loader.js +277 -0
- package/apps/runtime/dist/plugins/migrations.js +69 -0
- package/apps/runtime/dist/plugins/registry.js +105 -0
- package/apps/runtime/dist/routes/plugins.js +160 -0
- package/apps/runtime/dist/routes/runs.js +7 -3
- package/apps/runtime/dist/routes/studio.js +20 -0
- package/apps/runtime/dist/secrets/signing.js +21 -3
- package/apps/runtime/dist/services/runs.js +190 -28
- package/apps/runtime/dist/services/wizards.js +30 -4
- package/apps/runtime/dist/tools/index.js +2 -0
- package/apps/runtime/dist/tools/plugin.js +43 -0
- package/apps/web/dist/assets/ChangelogDialog-Co_9fs61.js +1 -0
- package/apps/web/dist/assets/ChatPanel-Br3nYkUc.js +1 -0
- package/apps/web/dist/assets/CreditsDialog-D3fZRqXH.js +1 -0
- package/apps/web/dist/assets/EditorPage-BSjQC6FE.js +2 -0
- package/apps/web/dist/assets/FlowPreview-CjUK5046.js +1 -0
- package/apps/web/dist/assets/HomePage-8v4HqIbP.js +1 -0
- package/apps/web/dist/assets/ImportWizard-DiV62Uno.js +1 -0
- package/apps/web/dist/assets/{LocalRuntime-DKqEzptp.js → LocalRuntime-CTyLGC98.js} +2 -2
- package/apps/web/dist/assets/NewWizardPage-BBL2yenw.js +2 -0
- package/apps/web/dist/assets/PublicRunner-BwtoEVlE.js +1 -0
- package/apps/web/dist/assets/RunnerView-DOizlTad.js +2 -0
- package/apps/web/dist/assets/SettingsPage-CBDv04YS.js +2 -0
- package/apps/web/dist/assets/SetupPage-CfXTOaIm.js +1 -0
- package/apps/web/dist/assets/SharePage-CgByqrAF.js +1 -0
- package/apps/web/dist/assets/api-KMi35npD.js +10 -0
- package/apps/web/dist/assets/{arrow-left-UpjOBfQp.js → arrow-left-hTN1VoH1.js} +1 -1
- package/apps/web/dist/assets/{arrow-right-CucQycEy.js → arrow-right-6FyN3e7D.js} +1 -1
- package/apps/web/dist/assets/changelog-03JgixQA.js +1 -0
- package/apps/web/dist/assets/check-CUpby_J9.js +1 -0
- package/apps/web/dist/assets/{createLucideIcon-DBu2Ovxj.js → createLucideIcon-q3MZMmnf.js} +1 -1
- package/apps/web/dist/assets/definition-DndaH5Ow.js +55 -0
- package/apps/web/dist/assets/engenty-Cm8yZLON.js +1 -0
- package/apps/web/dist/assets/esm-rJY59Ixx.js +7 -0
- package/apps/web/dist/assets/{external-link-CRg6DVdO.js → external-link-MAL7vJYj.js} +1 -1
- package/apps/web/dist/assets/{fluffy-engenty-CJ3wCQgT.js → fluffy-engenty-D7rFrNcJ.js} +2 -2
- package/apps/web/dist/assets/index-B9YVjvVY.css +1 -0
- package/apps/web/dist/assets/index-BmpnE7IO.js +10 -0
- package/apps/web/dist/assets/jsx-runtime-D3jfb0Ew.js +1 -0
- package/apps/web/dist/assets/meta-4R1Lnr25.js +1 -0
- package/apps/web/dist/assets/{mic-CeqQ89Dj.js → mic-Bvz4mA2j.js} +1 -1
- package/apps/web/dist/assets/oss-credits-roB62QJu.js +1 -0
- package/apps/web/dist/assets/{outputs-CHe6KRLc.js → outputs-COE_nvAw.js} +2 -2
- package/apps/web/dist/assets/plug-CLjyCoHB.js +1 -0
- package/apps/web/dist/assets/{plus-HA210wXA.js → plus-Du-Jx8aI.js} +1 -1
- package/apps/web/dist/assets/react-dom-DenslgJm.js +1 -0
- package/apps/web/dist/assets/session-B6-6VomQ.js +1 -0
- package/apps/web/dist/assets/type-Bj9H3eOK.js +1 -0
- package/apps/web/dist/assets/ui-Cs6lpsmj.js +1 -0
- package/apps/web/dist/assets/{upload-DsgLfQ3U.js → upload--J5bZhEM.js} +1 -1
- package/apps/web/dist/assets/{video-Dk1fuTDl.js → video-CjgmvRDk.js} +1 -1
- package/apps/web/dist/assets/{wand-sparkles-Bf97fs8T.js → wand-sparkles-CKxArWPY.js} +1 -1
- package/apps/web/dist/index.html +10 -7
- package/apps/web/dist/mcp/flow.html +748 -0
- package/modules/README.md +15 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +144 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.js +4 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +80 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.js +4 -0
- package/node_modules/@engenty-wizards/plugin-sdk/package.json +10 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +18 -6
- package/node_modules/@engenty-wizards/shared/dist/definition.js +22 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/use-wizard/SKILL.md +18 -0
- package/apps/web/dist/assets/ChangelogDialog-DURK75KD.js +0 -1
- package/apps/web/dist/assets/ChatPanel-DZhZJZCP.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-CLUOVBLG.js +0 -1
- package/apps/web/dist/assets/EditorPage-PGCEQdiC.js +0 -2
- package/apps/web/dist/assets/FlowPreview-CqJUNW13.js +0 -1
- package/apps/web/dist/assets/HomePage-CRkDMNM2.js +0 -1
- package/apps/web/dist/assets/ImportWizard-DX1YtFPv.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-BPJrYi2m.js +0 -2
- package/apps/web/dist/assets/PublicRunner-B3T244Ry.js +0 -1
- package/apps/web/dist/assets/RunnerView-DgYaOHIH.js +0 -2
- package/apps/web/dist/assets/SettingsPage-LBVqEwXs.js +0 -2
- package/apps/web/dist/assets/SetupPage-DAyJs-hK.js +0 -1
- package/apps/web/dist/assets/SharePage-oKGavxJt.js +0 -1
- package/apps/web/dist/assets/api-DbPn6Dge.js +0 -10
- package/apps/web/dist/assets/changelog-BJkYErYt.js +0 -1
- package/apps/web/dist/assets/check-Bjx41xbB.js +0 -1
- package/apps/web/dist/assets/definition-cy7uvY5L.js +0 -55
- package/apps/web/dist/assets/engenty-CWIFpgKq.js +0 -1
- package/apps/web/dist/assets/esm-ZHEGGS6s.js +0 -7
- package/apps/web/dist/assets/index-BRy5sz4a.js +0 -10
- package/apps/web/dist/assets/index-CQ_q9iMO.css +0 -1
- package/apps/web/dist/assets/jsx-runtime-Dk72oS4N.js +0 -1
- package/apps/web/dist/assets/meta-2YMHa2hT.js +0 -1
- package/apps/web/dist/assets/oss-credits-cPuZUFHD.js +0 -1
- package/apps/web/dist/assets/type-BzYgVWKL.js +0 -1
- 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;
|