wizards 0.1.10 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/apps/runtime/dist/agents/architect.js +6 -4
- package/apps/runtime/dist/app.js +2 -0
- package/apps/runtime/dist/authoring/guide.js +10 -2
- package/apps/runtime/dist/cli/environment.js +5 -0
- package/apps/runtime/dist/cli/home.js +1 -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/tools.js +2 -1
- 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/services/wizards.js +29 -3
- package/apps/runtime/dist/tools/index.js +2 -0
- package/apps/runtime/dist/tools/plugin.js +43 -0
- package/apps/web/dist/assets/ChangelogDialog-COqPzrKT.js +1 -0
- package/apps/web/dist/assets/{ChatPanel-BORDbEJt.js → ChatPanel-aQP8iz10.js} +1 -1
- package/apps/web/dist/assets/CreditsDialog-pRbTW7Xh.js +1 -0
- package/apps/web/dist/assets/EditorPage-xkq6pvw-.js +2 -0
- package/apps/web/dist/assets/FlowPreview-CDmcN1oX.js +1 -0
- package/apps/web/dist/assets/HomePage-DUAiS9pg.js +1 -0
- package/apps/web/dist/assets/ImportWizard-pzljVvcr.js +1 -0
- package/apps/web/dist/assets/{LocalRuntime-B33qoSb2.js → LocalRuntime-DPJMJlWc.js} +2 -2
- package/apps/web/dist/assets/NewWizardPage-TwYgdbKl.js +2 -0
- package/apps/web/dist/assets/PublicRunner-DoZdJuyW.js +1 -0
- package/apps/web/dist/assets/RunnerView-XKosfgO-.js +2 -0
- package/apps/web/dist/assets/SettingsPage-C0_peJuV.js +2 -0
- package/apps/web/dist/assets/SetupPage-BFQ0V__v.js +1 -0
- package/apps/web/dist/assets/SharePage-C1yg5bc2.js +1 -0
- package/apps/web/dist/assets/api-DijQGkp-.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-sa4SXBrg.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-BAObuqzs.js +10 -0
- package/apps/web/dist/assets/index-BOl3401k.css +1 -0
- package/apps/web/dist/assets/jsx-runtime-D3jfb0Ew.js +1 -0
- package/apps/web/dist/assets/{meta-DmwCkikb.js → meta-ijlGGCOr.js} +1 -1
- package/apps/web/dist/assets/{mic-CeqQ89Dj.js → mic-Bvz4mA2j.js} +1 -1
- package/apps/web/dist/assets/{oss-credits-cPuZUFHD.js → oss-credits-ClwxOPrf.js} +1 -1
- package/apps/web/dist/assets/{outputs-BJ6FXJP6.js → outputs-37WFkpRS.js} +2 -2
- 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-BerOchdC.js +1 -0
- package/apps/web/dist/assets/{type-BzYgVWKL.js → type-c_dt5Ne8.js} +1 -1
- package/apps/web/dist/assets/ui-DKc92qJ1.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 +9 -7
- package/apps/web/dist/install.sh +2 -3
- 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 +5 -1
- package/apps/web/dist/assets/ChangelogDialog-UOklVDXF.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-CLUOVBLG.js +0 -1
- package/apps/web/dist/assets/EditorPage-BM5yfvZ6.js +0 -2
- package/apps/web/dist/assets/FlowPreview-DIIeD1wY.js +0 -1
- package/apps/web/dist/assets/HomePage-BaVY5u1C.js +0 -1
- package/apps/web/dist/assets/ImportWizard-CD-tOsz2.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-B0jx_Ptd.js +0 -2
- package/apps/web/dist/assets/PublicRunner-Bj_gFWxk.js +0 -1
- package/apps/web/dist/assets/RunnerView-__E0Cpbc.js +0 -2
- package/apps/web/dist/assets/SettingsPage-DzhhUY31.js +0 -2
- package/apps/web/dist/assets/SetupPage-BSe-mh8G.js +0 -1
- package/apps/web/dist/assets/SharePage-Duany-Lu.js +0 -1
- package/apps/web/dist/assets/api-DbPn6Dge.js +0 -10
- package/apps/web/dist/assets/changelog-14GVP0cv.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-Bk5dFCAD.js +0 -7
- package/apps/web/dist/assets/index-BYV0HYlP.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/ui-tO4ZXbfh.js +0 -1
package/README.md
CHANGED
|
@@ -177,6 +177,9 @@ More settings: [.env.example](.env.example).
|
|
|
177
177
|
Development server, checks, how it is built, the managed mode:
|
|
178
178
|
[docs/development.md](docs/development.md).
|
|
179
179
|
|
|
180
|
+
Plugins add tools for steps, routes with their own tables and pages in the studio, without
|
|
181
|
+
changing the runtime: [docs/plugins.md](docs/plugins.md).
|
|
182
|
+
|
|
180
183
|
## License
|
|
181
184
|
|
|
182
185
|
[FSL-1.1-MIT](LICENSE): use it for yourself and your company, run it, change it — just don't
|
|
@@ -3,20 +3,22 @@ import { isTextMime } from "@engenty-wizards/shared/workspace";
|
|
|
3
3
|
import { Agent } from "@mastra/core/agent";
|
|
4
4
|
import { createTool } from "@mastra/core/tools";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { connectorsLine, exampleWizard, mcpServersLine, PRINCIPLES, SCHEMA_DOC, WIDGET_GUIDE, } from "../authoring/guide.js";
|
|
6
|
+
import { connectorsLine, exampleWizard, mcpServersLine, PRINCIPLES, pluginToolsLine, SCHEMA_DOC, WIDGET_GUIDE, } from "../authoring/guide.js";
|
|
7
7
|
import { wizardOpSchema } from "../authoring/ops.js";
|
|
8
8
|
import { importFromRegistry, listConnectors, searchRegistry } from "../connectors/external.js";
|
|
9
9
|
import { attachTools, costOf, gatewayTools, textModel } from "../models.js";
|
|
10
|
+
import { pluginToolsOf } from "../plugins/registry.js";
|
|
10
11
|
import { htmlToMarkdown } from "../render/convert.js";
|
|
11
12
|
import { ServiceError } from "../services/errors.js";
|
|
12
13
|
import { deleteFile, listFiles, readFileText, writeFile } from "../services/files.js";
|
|
13
14
|
import { checkDraftWidget } from "../services/widgets.js";
|
|
14
15
|
import { editWizard, ownedWizard, writeDraft } from "../services/wizards.js";
|
|
16
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
15
17
|
import { assertPublicUrl, safeFetch } from "../tools/net-guard.js";
|
|
16
18
|
import { runScript } from "../widgets/script.js";
|
|
17
19
|
import { thoughtLine } from "./thought.js";
|
|
18
20
|
const MAX_STEPS = 45;
|
|
19
|
-
function systemPrompt(mcp, connectors) {
|
|
21
|
+
function systemPrompt(mcp, connectors, pluginTools) {
|
|
20
22
|
return `You design wizards for "engenty wizards": a page-by-page flow an end user walks through, where AI steps research, write, draw images, render video, build documents, dashboards and interactive widgets, or write into other systems.
|
|
21
23
|
|
|
22
24
|
You talk to the ADMIN who builds the wizard. Answer in the admin's language, briefly and warmly. Never mention JSON, ids, schemas, files or templates to them — talk about pages, questions, steps, the widget and results.
|
|
@@ -27,7 +29,7 @@ ${WIDGET_GUIDE}
|
|
|
27
29
|
|
|
28
30
|
${mcpServersLine(mcp)}
|
|
29
31
|
|
|
30
|
-
${connectorsLine(connectors)}
|
|
32
|
+
${connectorsLine(connectors)}${pluginToolsLine(pluginTools)}
|
|
31
33
|
|
|
32
34
|
Example of a complete wizard:
|
|
33
35
|
\`\`\`json
|
|
@@ -341,7 +343,7 @@ export async function runArchitect(input) {
|
|
|
341
343
|
// The system prompt is long and identical on every turn: cache it.
|
|
342
344
|
instructions: {
|
|
343
345
|
role: "system",
|
|
344
|
-
content: systemPrompt(input.mcpServers, input.connectors),
|
|
346
|
+
content: systemPrompt(input.mcpServers, input.connectors, await pluginToolsOf(currentTenant())),
|
|
345
347
|
providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
|
|
346
348
|
},
|
|
347
349
|
model: resolved.model,
|
package/apps/runtime/dist/app.js
CHANGED
|
@@ -18,6 +18,7 @@ import { PLUGIN_NAME, pluginArchive, pluginMarketplace } from "./plugin.js";
|
|
|
18
18
|
import { apiRoutes } from "./routes/api.js";
|
|
19
19
|
import { internalRoutes } from "./routes/internal.js";
|
|
20
20
|
import { marketplaceStudio } from "./routes/marketplace.js";
|
|
21
|
+
import { pluginRoutes } from "./routes/plugins.js";
|
|
21
22
|
import { connectCallback, publicRoutes, runRoutes, shareRoutes } from "./routes/runs.js";
|
|
22
23
|
import { studio } from "./routes/studio.js";
|
|
23
24
|
import { wizardStream } from "./routes/wizard-stream.js";
|
|
@@ -99,6 +100,7 @@ app.on(["GET", "POST", "DELETE"], "/api/mcp", (c) => mcp(c.req.raw));
|
|
|
99
100
|
app.on(["GET", "POST", "DELETE"], "/api/mcp/bridge/:token", (c) => bridgeRequest(c.req.param("token"), c.req.raw));
|
|
100
101
|
app.route("/api/studio/wizards", wizardStream);
|
|
101
102
|
app.route("/api/studio/marketplace", marketplaceStudio);
|
|
103
|
+
app.route("/api/studio/plugins", pluginRoutes);
|
|
102
104
|
app.route("/api/studio", studio);
|
|
103
105
|
app.route("/api/v1", apiRoutes);
|
|
104
106
|
app.route("/api/internal", internalRoutes);
|
|
@@ -218,6 +218,14 @@ export function connectorsLine(connectors) {
|
|
|
218
218
|
});
|
|
219
219
|
return `Connectors a wizard of this project can use (built in, or imported from the integrations registry; find more with find_connectors + import_connector):\n${lines.join("\n")}`;
|
|
220
220
|
}
|
|
221
|
+
/** The tools this app's plugins add; nothing where there are none. */
|
|
222
|
+
export function pluginToolsLine(tools) {
|
|
223
|
+
if (!tools.length) {
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
const lines = tools.map((t) => `- ${t.id} — ${t.description}`);
|
|
227
|
+
return `\n\nTools of this app's plugins. An agent step lists one in "tools" by its full id, next to the built-in ones; a wizard that uses one runs only where that plugin is installed:\n${lines.join("\n")}`;
|
|
228
|
+
}
|
|
221
229
|
export function mcpServersLine(mcp) {
|
|
222
230
|
return `Project MCP servers available to agent steps: ${mcp.length ? mcp.map((m) => `${m.id} (${m.name})`).join(", ") : "none — if the admin wants to write into an external system that needs one, build the step with http or browser, and tell them they can connect a server in the project settings"}.`;
|
|
223
231
|
}
|
|
@@ -225,7 +233,7 @@ export function exampleWizard() {
|
|
|
225
233
|
return JSON.stringify(EXAMPLE_WIZARD, null, 2);
|
|
226
234
|
}
|
|
227
235
|
/** The guide an MCP client reads before it writes a wizard. */
|
|
228
|
-
export function authoringGuide(mcp, connectors = []) {
|
|
236
|
+
export function authoringGuide(mcp, connectors = [], pluginTools = []) {
|
|
229
237
|
return `engenty wizards: a wizard is a page-by-page flow an end user walks through on a shared link. Pages ask questions; AI steps research, write, draw images, render video, build documents and dashboards, or write into other systems. You author the wizard as JSON; the person who asked you is the ADMIN, the people who later open the link are END USERS.
|
|
230
238
|
|
|
231
239
|
${SCHEMA_DOC}
|
|
@@ -234,7 +242,7 @@ ${WIDGET_GUIDE}
|
|
|
234
242
|
|
|
235
243
|
${mcpServersLine(mcp)}
|
|
236
244
|
|
|
237
|
-
${connectorsLine(connectors)}
|
|
245
|
+
${connectorsLine(connectors)}${pluginToolsLine(pluginTools)}
|
|
238
246
|
|
|
239
247
|
Models: a step names the KIND of model it needs, never a model. Pick the cheapest class that does the job:
|
|
240
248
|
- "classifier": routing, yes/no, picking from options, pulling a few values out of text.
|
|
@@ -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",
|
|
@@ -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. */
|
|
@@ -64,8 +64,21 @@ async function openTenant(id) {
|
|
|
64
64
|
const db = makeTenantDb(client);
|
|
65
65
|
// Every database is brought to the current schema when it is first opened.
|
|
66
66
|
await migrate(db, { migrationsFolder: migrationsFolder("tenant") });
|
|
67
|
+
for (const hook of openHooks) {
|
|
68
|
+
await hook(id, client);
|
|
69
|
+
}
|
|
67
70
|
return db;
|
|
68
71
|
}
|
|
72
|
+
const openHooks = [];
|
|
73
|
+
/** Runs for every tenant database right after it was opened and migrated (plugins' own tables). */
|
|
74
|
+
export function onTenantOpen(hook) {
|
|
75
|
+
openHooks.push(hook);
|
|
76
|
+
}
|
|
77
|
+
/** The tenant databases this process has open, once each of them finished opening. */
|
|
78
|
+
export async function openTenantClients() {
|
|
79
|
+
await Promise.allSettled(opened.values());
|
|
80
|
+
return [...clients.entries()];
|
|
81
|
+
}
|
|
69
82
|
/** The only way to a tenant's tables. Opens the database once per process and migrates it then. */
|
|
70
83
|
export function tenantDb(id) {
|
|
71
84
|
let db = opened.get(id);
|
|
@@ -10,6 +10,7 @@ import { saveAsset } from "../files/storage.js";
|
|
|
10
10
|
import { managed, tenantInfo } from "../manage.js";
|
|
11
11
|
import { hasFfmpeg } from "../media/ffmpeg.js";
|
|
12
12
|
import { ModelUnavailableError } from "../models.js";
|
|
13
|
+
import { runEnded } from "../plugins/events.js";
|
|
13
14
|
import { projectFiles } from "../services/project-files.js";
|
|
14
15
|
import { projectProfile } from "../services/projects.js";
|
|
15
16
|
import { listRows, scopeOf } from "../store/index.js";
|
|
@@ -112,6 +113,8 @@ async function updateRun(runId, patch) {
|
|
|
112
113
|
// A run that ended needs no credits held for it any more.
|
|
113
114
|
if (patch.status === "done" || patch.status === "failed" || patch.status === "cancelled") {
|
|
114
115
|
await releaseRun(runId);
|
|
116
|
+
// Plugins hear of it; the run does not wait for them.
|
|
117
|
+
void runEnded(runId, patch.status);
|
|
115
118
|
}
|
|
116
119
|
signalChanged(runId);
|
|
117
120
|
}
|
package/apps/runtime/dist/env.js
CHANGED
|
@@ -60,6 +60,11 @@ function agentOsInstalled() {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
/** A comma-separated list. */
|
|
64
|
+
const list = (key) => str(key)
|
|
65
|
+
.split(",")
|
|
66
|
+
.map((item) => item.trim())
|
|
67
|
+
.filter(Boolean);
|
|
63
68
|
const num = (key, fallback) => {
|
|
64
69
|
const v = Number(process.env[key]);
|
|
65
70
|
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
@@ -99,10 +104,7 @@ export const env = {
|
|
|
99
104
|
*/
|
|
100
105
|
signedOutUrl: str("SIGNED_OUT_URL"),
|
|
101
106
|
/** More host names this server answers under, besides APP_URL's and the loopback names. */
|
|
102
|
-
allowedHosts:
|
|
103
|
-
.split(",")
|
|
104
|
-
.map((h) => h.trim())
|
|
105
|
-
.filter(Boolean),
|
|
107
|
+
allowedHosts: list("ALLOWED_HOSTS"),
|
|
106
108
|
dataDir,
|
|
107
109
|
/** The control database; empty = a libSQL file in the data folder. */
|
|
108
110
|
databaseUrl: str("DATABASE_URL", ""),
|
|
@@ -190,6 +192,18 @@ export const env = {
|
|
|
190
192
|
secretAccessKey: str("S3_SECRET_ACCESS_KEY"),
|
|
191
193
|
},
|
|
192
194
|
turnstile: { siteKey: str("TURNSTILE_SITE_KEY"), secret: str("TURNSTILE_SECRET_KEY") },
|
|
195
|
+
/** Plugins (docs/plugins.md). Code in these folders runs with the runtime's own rights. */
|
|
196
|
+
plugins: {
|
|
197
|
+
/** Folders to look in, besides the `modules/` this runtime ships with. */
|
|
198
|
+
dirs: list("PLUGINS_DIR").map((dir) => resolve(dir)),
|
|
199
|
+
/** Managed: plugins every tenant has. The Manage-App switches further ones on per tenant. */
|
|
200
|
+
defaults: list("PLUGINS_DEFAULT"),
|
|
201
|
+
/**
|
|
202
|
+
* A plugin loads again when one of its files changes. Default: on where the runtime runs
|
|
203
|
+
* alone. A runtime of a Manage-App never reloads: it would hit every tenant.
|
|
204
|
+
*/
|
|
205
|
+
watch: !manageUrl && str("PLUGINS_WATCH", "1") === "1",
|
|
206
|
+
},
|
|
193
207
|
limits: {
|
|
194
208
|
visitorRunsPerHour: num("LIMIT_VISITOR_RUNS_PER_HOUR", 6),
|
|
195
209
|
defaultDailyRuns: num("LIMIT_DEFAULT_DAILY_RUNS", 50),
|
|
@@ -6,6 +6,7 @@ import { resumeInterruptedRuns } from "./engine/runner.js";
|
|
|
6
6
|
import { basePath, env } from "./env.js";
|
|
7
7
|
import { managed } from "./manage.js";
|
|
8
8
|
import { loadCatalog, loadLocalModels } from "./models.js";
|
|
9
|
+
import { loadPlugins } from "./plugins/loader.js";
|
|
9
10
|
import { closeBrowser } from "./render/chromium.js";
|
|
10
11
|
import { clearRunning, writeRunning } from "./running.js";
|
|
11
12
|
import { syncMarketplace } from "./services/marketplace.js";
|
|
@@ -14,6 +15,8 @@ import { purgeUnusedStores } from "./store/index.js";
|
|
|
14
15
|
import { LOCAL_TENANT } from "./tenants/tenant.js";
|
|
15
16
|
import { onRestart } from "./update.js";
|
|
16
17
|
await migrateControlDb();
|
|
18
|
+
// Before any tenant database opens: each gets the plugins' own tables as it does.
|
|
19
|
+
await loadPlugins();
|
|
17
20
|
if (managed) {
|
|
18
21
|
// Every tenant database is brought to this release's schema; a new one migrates when first opened.
|
|
19
22
|
void migrateAllTenants().then(({ migrated, failed }) => {
|
|
@@ -3,6 +3,7 @@ import { authoringGuide } from "../authoring/guide.js";
|
|
|
3
3
|
import { wizardOpSchema } from "../authoring/ops.js";
|
|
4
4
|
import { importFromRegistry, listConnectors, searchRegistry } from "../connectors/external.js";
|
|
5
5
|
import { RunConflict } from "../engine/runner.js";
|
|
6
|
+
import { pluginToolsOf } from "../plugins/registry.js";
|
|
6
7
|
import { ServiceError } from "../services/errors.js";
|
|
7
8
|
import { deleteFile, listFiles, readFileText, writeFile } from "../services/files.js";
|
|
8
9
|
import { marketplaceWizard, searchMarketplace } from "../services/marketplace.js";
|
|
@@ -79,7 +80,7 @@ export function registerTools(server, who) {
|
|
|
79
80
|
const project = projectId
|
|
80
81
|
? await ownedProject(who.userId, projectId)
|
|
81
82
|
: await defaultProject(who.userId);
|
|
82
|
-
return authoringGuide(project.mcpServers.map((s) => ({ id: s.id, name: s.name })), await listConnectors(project.id));
|
|
83
|
+
return authoringGuide(project.mcpServers.map((s) => ({ id: s.id, name: s.name })), await listConnectors(project.id), await pluginToolsOf(who.tenantId));
|
|
83
84
|
});
|
|
84
85
|
// --- connectors ------------------------------------------------------------
|
|
85
86
|
tool("find_connectors", "wizards:read", {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join, resolve, sep } from "node:path";
|
|
3
|
+
import { PLUGIN_ID } from "@engenty-wizards/shared/definition";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/**
|
|
6
|
+
* Finding plugins. A folder of plugins holds, in any mix:
|
|
7
|
+
*
|
|
8
|
+
* notify.ts one file: the server half, its id is the file's name
|
|
9
|
+
* contacts/engenty.plugin.json a folder with a manifest: `src/plugin.ts`, `dist/client.js`
|
|
10
|
+
* hello/index.ts a folder without one: the server half, its id is the folder's name
|
|
11
|
+
* node_modules/<package> packages installed there that carry a manifest
|
|
12
|
+
*/
|
|
13
|
+
export const MANIFEST_FILE = "engenty.plugin.json";
|
|
14
|
+
/** Further fields (an engenty module's `provides`, `capabilities`, …) are kept as they are. */
|
|
15
|
+
const manifestSchema = z.looseObject({
|
|
16
|
+
id: z.string().regex(PLUGIN_ID),
|
|
17
|
+
name: z.string().min(1),
|
|
18
|
+
version: z.string().default("0.0.0"),
|
|
19
|
+
description: z.string().optional(),
|
|
20
|
+
server: z.string().optional(),
|
|
21
|
+
studio: z.string().optional(),
|
|
22
|
+
styles: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
/** `.mjs` is left out: Node keeps such a file once imported, so a reload would not read it again. */
|
|
25
|
+
const CODE = new Set([".ts", ".js"]);
|
|
26
|
+
const SERVER_ENTRIES = ["src/plugin.ts", "src/plugin.js", "dist/plugin.js"];
|
|
27
|
+
const INDEX_ENTRIES = ["index.ts", "index.js"];
|
|
28
|
+
const isDir = (path) => statSync(path, { throwIfNoEntry: false })?.isDirectory() ?? false;
|
|
29
|
+
const firstExisting = (root, names) => names.map((name) => join(root, name)).find((path) => existsSync(path)) ?? null;
|
|
30
|
+
/** A path of the manifest must stay inside the plugin's folder. */
|
|
31
|
+
function inside(root, relative) {
|
|
32
|
+
const path = resolve(root, relative);
|
|
33
|
+
return path.startsWith(root + sep) && existsSync(path) ? path : null;
|
|
34
|
+
}
|
|
35
|
+
function fromManifest(root) {
|
|
36
|
+
const parsed = manifestSchema.safeParse(JSON.parse(readFileSync(join(root, MANIFEST_FILE), "utf8")));
|
|
37
|
+
if (!parsed.success) {
|
|
38
|
+
throw new Error(`${MANIFEST_FILE}: ${z.prettifyError(parsed.error)}`);
|
|
39
|
+
}
|
|
40
|
+
const manifest = parsed.data;
|
|
41
|
+
const entry = manifest.server
|
|
42
|
+
? inside(root, manifest.server)
|
|
43
|
+
: firstExisting(root, SERVER_ENTRIES);
|
|
44
|
+
if (manifest.server && !entry) {
|
|
45
|
+
throw new Error(`"server": ${manifest.server} is not a file of the plugin.`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id: manifest.id,
|
|
49
|
+
manifest,
|
|
50
|
+
root,
|
|
51
|
+
entry,
|
|
52
|
+
studio: inside(root, manifest.studio ?? "dist/client.js"),
|
|
53
|
+
styles: inside(root, manifest.styles ?? "dist/client.css"),
|
|
54
|
+
single: false,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** A plugin without a manifest: its id is the name of its file or folder. */
|
|
58
|
+
function bare(id, root, entry, single) {
|
|
59
|
+
if (!PLUGIN_ID.test(id)) {
|
|
60
|
+
throw new Error(`"${id}" is no plugin id: lower case, digits and "-", starting with a letter.`);
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
id,
|
|
64
|
+
manifest: { id, name: id, version: "0.0.0" },
|
|
65
|
+
root,
|
|
66
|
+
entry,
|
|
67
|
+
studio: null,
|
|
68
|
+
styles: null,
|
|
69
|
+
single,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** Packages below `node_modules` that are plugins: `<name>` and `@scope/<name>`. */
|
|
73
|
+
function packagesIn(nodeModules) {
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const name of readdirSync(nodeModules).sort()) {
|
|
76
|
+
if (name.startsWith(".")) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const path = join(nodeModules, name);
|
|
80
|
+
const candidates = name.startsWith("@")
|
|
81
|
+
? readdirSync(path)
|
|
82
|
+
.sort()
|
|
83
|
+
.map((child) => join(path, child))
|
|
84
|
+
: [path];
|
|
85
|
+
out.push(...candidates.filter((dir) => existsSync(join(dir, MANIFEST_FILE))));
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function sourceAt(path) {
|
|
90
|
+
if (!isDir(path)) {
|
|
91
|
+
const ext = extname(path);
|
|
92
|
+
return CODE.has(ext) && !path.endsWith(".d.ts")
|
|
93
|
+
? bare(basename(path, ext), resolve(path, ".."), path, true)
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
if (existsSync(join(path, MANIFEST_FILE))) {
|
|
97
|
+
return fromManifest(path);
|
|
98
|
+
}
|
|
99
|
+
const index = firstExisting(path, INDEX_ENTRIES);
|
|
100
|
+
return index ? bare(basename(path), path, index, false) : null;
|
|
101
|
+
}
|
|
102
|
+
/** The plugins in these folders. Of two with one id the first found stays. */
|
|
103
|
+
export function discoverPlugins(dirs) {
|
|
104
|
+
const found = new Map();
|
|
105
|
+
const problems = [];
|
|
106
|
+
const take = (path) => {
|
|
107
|
+
try {
|
|
108
|
+
const source = sourceAt(path);
|
|
109
|
+
if (!source) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const first = found.get(source.id);
|
|
113
|
+
if (first) {
|
|
114
|
+
problems.push({
|
|
115
|
+
path,
|
|
116
|
+
message: `Plugin "${source.id}" is already loaded from ${first.root}.`,
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
found.set(source.id, source);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
problems.push({ path, message: err.message });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
for (const dir of dirs) {
|
|
127
|
+
if (!isDir(dir)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const name of readdirSync(dir).sort()) {
|
|
131
|
+
if (name.startsWith(".") || name.startsWith("_")) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const path = join(dir, name);
|
|
135
|
+
if (name === "node_modules") {
|
|
136
|
+
packagesIn(path).forEach(take);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
take(path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { found: [...found.values()], problems };
|
|
144
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db, schema } from "../db/client.js";
|
|
3
|
+
import { currentTenant } from "../tenants/tenant.js";
|
|
4
|
+
import { pluginsOf } from "./registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Tells the tenant's plugins that a run reached its end. A plugin cannot fail a run: what a
|
|
7
|
+
* listener throws is logged and nothing more.
|
|
8
|
+
*/
|
|
9
|
+
export async function runEnded(runId, status) {
|
|
10
|
+
try {
|
|
11
|
+
const event = `run.${status}`;
|
|
12
|
+
const listening = (await pluginsOf(currentTenant())).filter((p) => p.listeners[event]?.length);
|
|
13
|
+
if (!listening.length) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const run = await db.query.run.findFirst({ where: eq(schema.run.id, runId) });
|
|
17
|
+
const wizard = run && (await db.query.wizard.findFirst({ where: eq(schema.wizard.id, run.wizardId) }));
|
|
18
|
+
if (!run || !wizard) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const payload = {
|
|
22
|
+
run: { id: run.id, wizardId: run.wizardId, mode: run.mode, status },
|
|
23
|
+
wizard: { id: wizard.id, title: run.definition.title, projectId: wizard.projectId },
|
|
24
|
+
values: run.state.values,
|
|
25
|
+
};
|
|
26
|
+
for (const plugin of listening) {
|
|
27
|
+
for (const listener of plugin.listeners[event] ?? []) {
|
|
28
|
+
try {
|
|
29
|
+
await listener(payload);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error(`[plugin ${plugin.source.id}] ${event}:`, err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
console.error("[plugins] run event:", err);
|
|
39
|
+
}
|
|
40
|
+
}
|