wizards 0.2.26 → 0.2.27
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/apps/runtime/dist/agents/project-assistant.js +10 -1
- package/apps/runtime/dist/authoring/guide.js +7 -3
- package/apps/runtime/dist/connectors/plugin-connections.js +248 -0
- package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
- package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
- package/apps/runtime/dist/db/schema.js +21 -0
- package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +25 -3
- package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +29 -1
- package/apps/runtime/dist/engine/input.js +10 -1
- package/apps/runtime/dist/mcp/tools.js +3 -2
- package/apps/runtime/dist/plugins/loader.js +22 -1
- package/apps/runtime/dist/plugins/registry.js +10 -0
- package/apps/runtime/dist/routes/runs.js +11 -3
- package/apps/runtime/dist/routes/studio.js +3 -1
- package/apps/runtime/dist/services/marketplace.js +134 -3
- package/apps/runtime/dist/services/runs.js +43 -15
- package/apps/runtime/dist/services/wizards.js +4 -2
- package/apps/runtime/dist/tools/plugin.js +29 -1
- package/apps/web/dist/assets/Account-DW396ig5.js +1 -0
- package/apps/web/dist/assets/{ChangelogDialog-CdtQWq0m.js → ChangelogDialog-BdZ1SmEd.js} +1 -1
- package/apps/web/dist/assets/CreditsDialog-fOYdoev3.js +1 -0
- package/apps/web/dist/assets/EditorPage-CAARFsaI.js +3 -0
- package/apps/web/dist/assets/{FlowPreview-IZcNrBJi.js → FlowPreview-U2AEnKlX.js} +1 -1
- package/apps/web/dist/assets/{Harness-Cj-k02TA.js → Harness-CtDlm_2b.js} +2 -2
- package/apps/web/dist/assets/HomePage-Bd-mkLxW.js +1 -0
- package/apps/web/dist/assets/ImportWizard-BMPGjd_t.js +1 -0
- package/apps/web/dist/assets/LocalRuntime-DjSNCRnV.js +1 -0
- package/apps/web/dist/assets/NewWizardPage-DZPcDR9i.js +2 -0
- package/apps/web/dist/assets/ProjectPage-_LAtG3tn.js +2 -0
- package/apps/web/dist/assets/PublicRunner-6Iqp0DmU.js +1 -0
- package/apps/web/dist/assets/RunDrawer-CtQoY3Ha.js +4 -0
- package/apps/web/dist/assets/RunnerView-BLOCNZaZ.js +2 -0
- package/apps/web/dist/assets/SettingsPage-Z6skBf6I.js +1 -0
- package/apps/web/dist/assets/SetupPage-4-Lic4pT.js +1 -0
- package/apps/web/dist/assets/SharePage-DfdhXxZm.js +1 -0
- package/apps/web/dist/assets/arrow-right-BOtqnsA1.js +1 -0
- package/apps/web/dist/assets/brand-CCAI9p0f.js +2 -0
- package/apps/web/dist/assets/changelog-DQM0oz9S.js +1 -0
- package/apps/web/dist/assets/circle-alert-BC6xmJ9r.js +1 -0
- package/apps/web/dist/assets/definition-D5V79jHA.js +1 -0
- package/apps/web/dist/assets/esm-FEWi61F_.js +7 -0
- package/apps/web/dist/assets/external-link-wKL1HE6G.js +1 -0
- package/apps/web/dist/assets/image-BMdSChOR.js +1 -0
- package/apps/web/dist/assets/index-BOp2fz-2.css +1 -0
- package/apps/web/dist/assets/index-YJMooz0J.js +132 -0
- package/apps/web/dist/assets/lock-BpqzLMuv.js +1 -0
- package/apps/web/dist/assets/meta-BqZ7SlZz.js +1 -0
- package/apps/web/dist/assets/pen-line-DM-u0dES.js +1 -0
- package/apps/web/dist/assets/refresh-cw-Bk7HkOIM.js +1 -0
- package/apps/web/dist/assets/settings-menu-BM_NfZfn.js +1 -0
- package/apps/web/dist/assets/star-JHzYYl5R.js +1 -0
- package/apps/web/dist/assets/trash-CdLg-Ela.js +1 -0
- package/apps/web/dist/assets/ui-8QmvuC9u.js +9 -0
- package/apps/web/dist/assets/upload-CL1R6shM.js +1 -0
- package/apps/web/dist/assets/video-BioLmXNO.js +1 -0
- package/apps/web/dist/assets/volume-2-_IOfPNCv.js +1 -0
- package/apps/web/dist/assets/wand-sparkles-DYtDQSJZ.js +1 -0
- package/apps/web/dist/index.html +4 -5
- package/apps/web/dist/mcp/flow.html +57 -57
- package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +85 -0
- package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +23 -0
- package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +28 -1
- package/node_modules/@engenty-wizards/shared/dist/definition.js +85 -5
- package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +5 -0
- package/package.json +1 -1
- package/apps/web/dist/assets/Account-Cnksw5Ea.js +0 -1
- package/apps/web/dist/assets/CreditsDialog-Ti43boCN.js +0 -1
- package/apps/web/dist/assets/EditorPage-N7EzlRSj.js +0 -3
- package/apps/web/dist/assets/HomePage-bjdKkt84.js +0 -1
- package/apps/web/dist/assets/ImportWizard-DagFXYPM.js +0 -1
- package/apps/web/dist/assets/LocalRuntime-Boi_zqQJ.js +0 -1
- package/apps/web/dist/assets/NewWizardPage-DO5J8OpZ.js +0 -2
- package/apps/web/dist/assets/ProjectPage-DdG9wYsd.js +0 -2
- package/apps/web/dist/assets/PublicRunner-B4ts9rT6.js +0 -1
- package/apps/web/dist/assets/RunDrawer-LnVfCiYO.js +0 -4
- package/apps/web/dist/assets/RunnerView-DwffK7AA.js +0 -70
- package/apps/web/dist/assets/SettingsPage-Cyx_jTlp.js +0 -1
- package/apps/web/dist/assets/SetupPage-CBDD4rpC.js +0 -1
- package/apps/web/dist/assets/SharePage-70niRKaO.js +0 -1
- package/apps/web/dist/assets/api-C4M6cbpW.js +0 -10
- package/apps/web/dist/assets/arrow-right-6FyN3e7D.js +0 -1
- package/apps/web/dist/assets/changelog-COyL1B5X.js +0 -1
- package/apps/web/dist/assets/check-CUpby_J9.js +0 -1
- package/apps/web/dist/assets/chevron-down-DymW-2BP.js +0 -1
- package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +0 -1
- package/apps/web/dist/assets/createLucideIcon-q3MZMmnf.js +0 -1
- package/apps/web/dist/assets/definition-DoCLtZZL.js +0 -55
- package/apps/web/dist/assets/esm-CfW8Rmtq.js +0 -7
- package/apps/web/dist/assets/external-link-MAL7vJYj.js +0 -1
- package/apps/web/dist/assets/image-CC_2mJd6.js +0 -1
- package/apps/web/dist/assets/index-CJ0YJ5HC.css +0 -1
- package/apps/web/dist/assets/index-DMKXnSf-.js +0 -10
- package/apps/web/dist/assets/lock-DZL2N67v.js +0 -1
- package/apps/web/dist/assets/meta-6jZygxJq.js +0 -1
- package/apps/web/dist/assets/pen-line-NK46C_nG.js +0 -1
- package/apps/web/dist/assets/plus-Du-Jx8aI.js +0 -1
- package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +0 -1
- package/apps/web/dist/assets/settings-menu-Dph4yXbO.js +0 -1
- package/apps/web/dist/assets/star-UHmZXpjA.js +0 -1
- package/apps/web/dist/assets/trash-0IafyhZB.js +0 -1
- package/apps/web/dist/assets/ui-BS348L4M.js +0 -1
- package/apps/web/dist/assets/upload-DNRENWzQ.js +0 -1
- package/apps/web/dist/assets/video-9fDPRPCv.js +0 -1
- package/apps/web/dist/assets/volume-2-DE67_rue.js +0 -1
- package/apps/web/dist/assets/wand-sparkles-IEd4jkyn.js +0 -1
|
@@ -497,3 +497,24 @@ export const marketplaceStar = sqliteTable("marketplace_star", {
|
|
|
497
497
|
entryId: text("entry_id").notNull(),
|
|
498
498
|
createdAt: createdAt(),
|
|
499
499
|
}, (t) => [primaryKey({ columns: [t.tenantId, t.entryId] })]);
|
|
500
|
+
/**
|
|
501
|
+
* An account a plugin connected for a space (`server.connections`): a calendar, a mailbox. Not
|
|
502
|
+
* a wizard's: it outlives runs and people. `data` is encrypted.
|
|
503
|
+
*/
|
|
504
|
+
export const pluginConnection = sqliteTable("plugin_connection", {
|
|
505
|
+
id: text("id").primaryKey(),
|
|
506
|
+
plugin: text("plugin").notNull(),
|
|
507
|
+
projectId: text("project_id")
|
|
508
|
+
.notNull()
|
|
509
|
+
.references(() => project.id, { onDelete: "cascade" }),
|
|
510
|
+
/** The built-in connector: `google-calendar`, `microsoft-outlook`. */
|
|
511
|
+
connector: text("connector").notNull(),
|
|
512
|
+
/** Shown to the person: the account's address. */
|
|
513
|
+
label: text("label").notNull(),
|
|
514
|
+
/** The connector's actions it was connected for. */
|
|
515
|
+
actions: text("actions", { mode: "json" }).$type().notNull(),
|
|
516
|
+
data: text("data").notNull(),
|
|
517
|
+
createdBy: text("created_by"),
|
|
518
|
+
createdAt: createdAt(),
|
|
519
|
+
updatedAt: updatedAt(),
|
|
520
|
+
}, (t) => [index("plugin_connection_space").on(t.plugin, t.projectId)]);
|
|
@@ -59,6 +59,12 @@ const attendeesField = z
|
|
|
59
59
|
.array(z.string().describe("Attendee email address."))
|
|
60
60
|
.optional()
|
|
61
61
|
.describe("Optional list of attendee email addresses.");
|
|
62
|
+
const sendUpdatesField = z
|
|
63
|
+
.enum(["all", "externalOnly", "none"])
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Whom Google e-mails about the change: "all" attendees, "externalOnly" (outside the organisation) or "none" (default).');
|
|
66
|
+
/** `?sendUpdates=` for a write, when the caller asked Google to tell the attendees. */
|
|
67
|
+
const updatesQuery = (value) => value ? `?sendUpdates=${encodeURIComponent(value)}` : "";
|
|
62
68
|
export const calendarConnector = defineConnector({
|
|
63
69
|
actions: [
|
|
64
70
|
connectorAction({
|
|
@@ -237,12 +243,13 @@ export const calendarConnector = defineConnector({
|
|
|
237
243
|
group: "write",
|
|
238
244
|
handler: async (input, ctx) => {
|
|
239
245
|
const calendarId = input.calendar_id ?? "primary";
|
|
240
|
-
const created = await googleJson(ctx, `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events`, {
|
|
246
|
+
const created = await googleJson(ctx, `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events${updatesQuery(input.send_updates)}`, {
|
|
241
247
|
body: JSON.stringify({
|
|
242
248
|
end: toEventDateTime(input.end, input.time_zone),
|
|
243
249
|
start: toEventDateTime(input.start, input.time_zone),
|
|
244
250
|
summary: input.summary,
|
|
245
251
|
...(input.description ? { description: input.description } : {}),
|
|
252
|
+
...(input.location ? { location: input.location } : {}),
|
|
246
253
|
...(input.attendees?.length
|
|
247
254
|
? { attendees: input.attendees.map((email) => ({ email })) }
|
|
248
255
|
: {}),
|
|
@@ -264,7 +271,12 @@ export const calendarConnector = defineConnector({
|
|
|
264
271
|
.optional()
|
|
265
272
|
.describe("Optional event description/notes."),
|
|
266
273
|
end: endField,
|
|
274
|
+
location: z
|
|
275
|
+
.string()
|
|
276
|
+
.optional()
|
|
277
|
+
.describe("Where the event takes place: an address, a phone number or a meeting link."),
|
|
267
278
|
private_properties: privatePropertiesField,
|
|
279
|
+
send_updates: sendUpdatesField,
|
|
268
280
|
start: startField,
|
|
269
281
|
summary: z.string().describe("Event title."),
|
|
270
282
|
time_zone: timeZoneField,
|
|
@@ -284,6 +296,9 @@ export const calendarConnector = defineConnector({
|
|
|
284
296
|
if (input.description !== undefined) {
|
|
285
297
|
patch.description = input.description;
|
|
286
298
|
}
|
|
299
|
+
if (input.location !== undefined) {
|
|
300
|
+
patch.location = input.location;
|
|
301
|
+
}
|
|
287
302
|
if (input.start !== undefined) {
|
|
288
303
|
patch.start = toEventDateTime(input.start, input.time_zone);
|
|
289
304
|
}
|
|
@@ -296,7 +311,7 @@ export const calendarConnector = defineConnector({
|
|
|
296
311
|
if (input.private_properties !== undefined) {
|
|
297
312
|
patch.extendedProperties = { private: input.private_properties };
|
|
298
313
|
}
|
|
299
|
-
const updated = await googleJson(ctx, `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}`, {
|
|
314
|
+
const updated = await googleJson(ctx, `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}${updatesQuery(input.send_updates)}`, {
|
|
300
315
|
body: JSON.stringify(patch),
|
|
301
316
|
headers: { "Content-Type": "application/json" },
|
|
302
317
|
method: "PATCH",
|
|
@@ -314,7 +329,12 @@ export const calendarConnector = defineConnector({
|
|
|
314
329
|
.describe("New event description (replaces the existing one)."),
|
|
315
330
|
end: endField.optional(),
|
|
316
331
|
event_id: z.string().describe("Calendar event id to update."),
|
|
332
|
+
location: z
|
|
333
|
+
.string()
|
|
334
|
+
.optional()
|
|
335
|
+
.describe("New location (replaces the existing one)."),
|
|
317
336
|
private_properties: privatePropertiesField,
|
|
337
|
+
send_updates: sendUpdatesField,
|
|
318
338
|
start: startField.optional(),
|
|
319
339
|
summary: z.string().optional().describe("New event title."),
|
|
320
340
|
time_zone: timeZoneField,
|
|
@@ -322,6 +342,7 @@ export const calendarConnector = defineConnector({
|
|
|
322
342
|
.refine((v) => v.attendees !== undefined ||
|
|
323
343
|
v.description !== undefined ||
|
|
324
344
|
v.end !== undefined ||
|
|
345
|
+
v.location !== undefined ||
|
|
325
346
|
v.private_properties !== undefined ||
|
|
326
347
|
v.start !== undefined ||
|
|
327
348
|
v.summary !== undefined, { message: "Provide at least one field to update." }),
|
|
@@ -333,7 +354,7 @@ export const calendarConnector = defineConnector({
|
|
|
333
354
|
group: "destructive",
|
|
334
355
|
handler: async (input, ctx) => {
|
|
335
356
|
const calendarId = input.calendar_id ?? "primary";
|
|
336
|
-
await googleFetch(ctx, `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}`, { method: "DELETE" });
|
|
357
|
+
await googleFetch(ctx, `${CALENDAR_API}/calendars/${encodeURIComponent(calendarId)}/events/${encodeURIComponent(input.event_id)}${updatesQuery(input.send_updates)}`, { method: "DELETE" });
|
|
337
358
|
return {
|
|
338
359
|
calendar_id: calendarId,
|
|
339
360
|
deleted: true,
|
|
@@ -344,6 +365,7 @@ export const calendarConnector = defineConnector({
|
|
|
344
365
|
inputSchema: z.object({
|
|
345
366
|
calendar_id: calendarIdField,
|
|
346
367
|
event_id: z.string().describe("Calendar event id to delete."),
|
|
368
|
+
send_updates: sendUpdatesField,
|
|
347
369
|
}),
|
|
348
370
|
providerScopes: [SCOPE_EVENTS],
|
|
349
371
|
summary: "Delete a calendar event",
|
|
@@ -25,6 +25,7 @@ function mapEventSummary(e) {
|
|
|
25
25
|
isAllDay: e.isAllDay ?? false,
|
|
26
26
|
location: e.location?.displayName ?? null,
|
|
27
27
|
organizer: mapRecipient(e.organizer),
|
|
28
|
+
show_as: e.showAs ?? null,
|
|
28
29
|
start: e.start ?? null,
|
|
29
30
|
subject: e.subject ?? null,
|
|
30
31
|
};
|
|
@@ -93,7 +94,7 @@ function draftMessagePayload(input) {
|
|
|
93
94
|
toRecipients: toRecipients(input.to),
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
|
-
const EVENT_SELECT = "id,subject,start,end,location,organizer,isAllDay";
|
|
97
|
+
const EVENT_SELECT = "id,subject,start,end,location,organizer,isAllDay,showAs";
|
|
97
98
|
async function fetchEventDetail(ctx, eventId) {
|
|
98
99
|
const e = await graphJson(ctx, `/me/events/${encodeURIComponent(eventId)}?$select=${EVENT_SELECT},attendees,body,webLink`);
|
|
99
100
|
return mapEventDetail(e);
|
|
@@ -232,6 +233,10 @@ export const microsoftOutlookConnector = defineConnector({
|
|
|
232
233
|
.optional()
|
|
233
234
|
.describe("Window start (ISO 8601). Provide both start and end to query a window."),
|
|
234
235
|
top: topField,
|
|
236
|
+
all: z
|
|
237
|
+
.boolean()
|
|
238
|
+
.optional()
|
|
239
|
+
.describe("With start and end: every event of the window (up to 1000), not only the first `top`. For free/busy."),
|
|
235
240
|
}),
|
|
236
241
|
providerScopes: ["Calendars.Read"],
|
|
237
242
|
run: async (input, ctx) => {
|
|
@@ -244,6 +249,20 @@ export const microsoftOutlookConnector = defineConnector({
|
|
|
244
249
|
if (input.start && input.end) {
|
|
245
250
|
params.set("startDateTime", input.start);
|
|
246
251
|
params.set("endDateTime", input.end);
|
|
252
|
+
if (input.all) {
|
|
253
|
+
// Page through the window, 100 at a time.
|
|
254
|
+
const events = [];
|
|
255
|
+
params.set("$top", "100");
|
|
256
|
+
for (let skip = 0; skip < 1000; skip += 100) {
|
|
257
|
+
params.set("$skip", String(skip));
|
|
258
|
+
const page = await graphJson(ctx, `${calendarViewPath(input.calendar_id)}?${params.toString()}`);
|
|
259
|
+
events.push(...(page.value ?? []));
|
|
260
|
+
if ((page.value ?? []).length < 100) {
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { events: events.map(mapEventSummary) };
|
|
265
|
+
}
|
|
247
266
|
path = `${calendarViewPath(input.calendar_id)}?${params.toString()}`;
|
|
248
267
|
}
|
|
249
268
|
const data = await graphJson(ctx, path);
|
|
@@ -320,6 +339,10 @@ export const microsoftOutlookConnector = defineConnector({
|
|
|
320
339
|
.optional()
|
|
321
340
|
.describe("Plain-text event description."),
|
|
322
341
|
calendar_id: calendarIdField,
|
|
342
|
+
location: z
|
|
343
|
+
.string()
|
|
344
|
+
.optional()
|
|
345
|
+
.describe("Where the event takes place: an address, a phone number or a meeting link."),
|
|
323
346
|
end: z
|
|
324
347
|
.string()
|
|
325
348
|
.describe("Event end, ISO 8601 date-time (e.g. 2026-07-04T15:00:00)."),
|
|
@@ -343,6 +366,7 @@ export const microsoftOutlookConnector = defineConnector({
|
|
|
343
366
|
? { body: { content: input.body_text, contentType: "Text" } }
|
|
344
367
|
: {}),
|
|
345
368
|
end: { dateTime: input.end, timeZone: input.time_zone },
|
|
369
|
+
...(input.location ? { location: { displayName: input.location } } : {}),
|
|
346
370
|
start: { dateTime: input.start, timeZone: input.time_zone },
|
|
347
371
|
subject: input.subject,
|
|
348
372
|
},
|
|
@@ -367,6 +391,7 @@ export const microsoftOutlookConnector = defineConnector({
|
|
|
367
391
|
.describe("New plain-text event description."),
|
|
368
392
|
end: z.string().optional().describe("New end, ISO 8601 date-time."),
|
|
369
393
|
event_id: z.string().describe("Graph event id to update."),
|
|
394
|
+
location: z.string().optional().describe("New location (replaces the existing one)."),
|
|
370
395
|
start: z.string().optional().describe("New start, ISO 8601 date-time."),
|
|
371
396
|
subject: z.string().optional().describe("New event subject/title."),
|
|
372
397
|
time_zone: z
|
|
@@ -392,6 +417,9 @@ export const microsoftOutlookConnector = defineConnector({
|
|
|
392
417
|
if (input.attendees !== undefined) {
|
|
393
418
|
patch.attendees = toAttendees(input.attendees);
|
|
394
419
|
}
|
|
420
|
+
if (input.location !== undefined) {
|
|
421
|
+
patch.location = { displayName: input.location };
|
|
422
|
+
}
|
|
395
423
|
if (Object.keys(patch).length === 0) {
|
|
396
424
|
throw new Error("update_event: no fields to update");
|
|
397
425
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDecidedField, isLocationValue, MAX_FILES, shownFields, } from "@engenty-wizards/shared/definition";
|
|
1
|
+
import { isDecidedField, isLocationValue, MAX_FILES, offeredSlot, shownFields, } from "@engenty-wizards/shared/definition";
|
|
2
2
|
function empty(v) {
|
|
3
3
|
return v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
4
4
|
}
|
|
@@ -20,6 +20,11 @@ function coerce(field, raw) {
|
|
|
20
20
|
return (Array.isArray(raw) ? raw : [raw])
|
|
21
21
|
.map(String)
|
|
22
22
|
.filter((v) => field.options?.includes(v));
|
|
23
|
+
// The offered time as written, also when the answer names the same moment in another offset.
|
|
24
|
+
case "slot": {
|
|
25
|
+
const v = String(raw).trim();
|
|
26
|
+
return v ? (offeredSlot(v, field.options ?? []) ?? v) : undefined;
|
|
27
|
+
}
|
|
23
28
|
case "image":
|
|
24
29
|
case "file": {
|
|
25
30
|
const ids = (Array.isArray(raw) ? raw : [raw])
|
|
@@ -153,6 +158,10 @@ export function readPageInput(step, input, context = {}) {
|
|
|
153
158
|
errors.push({ field: field.id, message: "Ungültige Auswahl" });
|
|
154
159
|
continue;
|
|
155
160
|
}
|
|
161
|
+
if (field.kind === "slot" && !empty(v) && !field.options?.includes(String(v))) {
|
|
162
|
+
errors.push({ field: field.id, message: "Dieser Termin wird nicht angeboten" });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
156
165
|
if (field.kind === "email" && !empty(v) && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(v))) {
|
|
157
166
|
errors.push({ field: field.id, message: "Keine gültige E-Mail-Adresse" });
|
|
158
167
|
continue;
|
|
@@ -143,7 +143,7 @@ export function registerTools(server, who) {
|
|
|
143
143
|
});
|
|
144
144
|
tool("list_starters", "wizards:read", {
|
|
145
145
|
title: "List starters",
|
|
146
|
-
description: "
|
|
146
|
+
description: "Ready-made wizards to start from: first those the space's plugins bring (marked with the plugin), then the marketplace's (tweet with image, video ad, research briefing, dashboard, invoice, offer …), with what each makes and needs. Give a query to search them; without one you get the starters.",
|
|
147
147
|
input: z.object({ query: z.string().max(200).optional() }),
|
|
148
148
|
readOnly: true,
|
|
149
149
|
}, async ({ query }) => (await searchMarketplace({ q: query ?? "", lang: "de", limit: 30 })).entries
|
|
@@ -155,6 +155,7 @@ export function registerTools(server, who) {
|
|
|
155
155
|
formats: s.formats,
|
|
156
156
|
useCases: s.useCases,
|
|
157
157
|
capabilities: s.capabilities,
|
|
158
|
+
...(s.plugin ? { plugin: s.plugin.name } : {}),
|
|
158
159
|
})));
|
|
159
160
|
tool("get_starter", "wizards:read", {
|
|
160
161
|
title: "Get starter",
|
|
@@ -401,7 +402,7 @@ While status is running, get_run with waitSeconds. Hosts with MCP Apps also show
|
|
|
401
402
|
}, async ({ runId, waitSeconds }) => runReport(who.userId, runId, waitSeconds));
|
|
402
403
|
tool("answer_page", "runs:test", {
|
|
403
404
|
title: "Answer page",
|
|
404
|
-
description: "Fill the page a run waits for: field id → value (items: an array of row objects; toggle: true/false; multiselect: an array). Returns the run, after waitSeconds.",
|
|
405
|
+
description: "Fill the page a run waits for: field id → value (items: an array of row objects; toggle: true/false; multiselect: an array; slot: one of the offered times, as given). Returns the run, after waitSeconds.",
|
|
405
406
|
input: z.object({
|
|
406
407
|
runId: z.string(),
|
|
407
408
|
stepId: z.string().describe("waitingFor.page"),
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, watch } from "node:fs";
|
|
2
2
|
import { join, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { PLUGIN_TOOL_NAME } from "@engenty-wizards/shared/definition";
|
|
4
|
+
import { PLUGIN_ID, PLUGIN_TOOL_NAME, wizardSchema } from "@engenty-wizards/shared/definition";
|
|
5
5
|
import { generateText, Output } from "ai";
|
|
6
6
|
import { createJiti } from "jiti";
|
|
7
7
|
import { readPage } from "../agents/website.js";
|
|
8
8
|
import { packageRoot } from "../cli/home.js";
|
|
9
|
+
import { pluginConnections } from "../connectors/plugin-connections.js";
|
|
9
10
|
import { db, onTenantOpen } from "../db/client.js";
|
|
10
11
|
import { parseDocument } from "../documents/parse.js";
|
|
11
12
|
import { env } from "../env.js";
|
|
@@ -98,6 +99,9 @@ function apiFor(record) {
|
|
|
98
99
|
}
|
|
99
100
|
record.assistantTools.set(tool.name, tool);
|
|
100
101
|
},
|
|
102
|
+
registerAssistantContext(context) {
|
|
103
|
+
record.assistantPages.push(context);
|
|
104
|
+
},
|
|
101
105
|
registerPublicRoute(route) {
|
|
102
106
|
record.publicRoutes.push(compileRoute(route));
|
|
103
107
|
},
|
|
@@ -153,6 +157,21 @@ function apiFor(record) {
|
|
|
153
157
|
object: (request.schema ? result.output : undefined),
|
|
154
158
|
};
|
|
155
159
|
},
|
|
160
|
+
connections: pluginConnections(source.id),
|
|
161
|
+
registerStarter(starter) {
|
|
162
|
+
if (!PLUGIN_ID.test(starter.id)) {
|
|
163
|
+
throw new Error(`Starter "${starter.id}": an id is lower case, digits and "-".`);
|
|
164
|
+
}
|
|
165
|
+
if (record.starters.has(starter.id)) {
|
|
166
|
+
throw new Error(`Starter "${starter.id}" is registered twice.`);
|
|
167
|
+
}
|
|
168
|
+
const parsed = wizardSchema.safeParse(starter.definition);
|
|
169
|
+
if (!parsed.success) {
|
|
170
|
+
const issue = parsed.error.issues[0];
|
|
171
|
+
throw new Error(`Starter "${starter.id}": ${issue?.path.join(".") || "definition"}: ${issue?.message}`);
|
|
172
|
+
}
|
|
173
|
+
record.starters.set(starter.id, { ...starter, definition: parsed.data });
|
|
174
|
+
},
|
|
156
175
|
onUnload(dispose) {
|
|
157
176
|
record.disposers.push(dispose);
|
|
158
177
|
},
|
|
@@ -186,8 +205,10 @@ async function load(source) {
|
|
|
186
205
|
record.tools.clear();
|
|
187
206
|
record.contexts = [];
|
|
188
207
|
record.assistantTools.clear();
|
|
208
|
+
record.assistantPages = [];
|
|
189
209
|
record.publicRoutes = [];
|
|
190
210
|
record.jobs.clear();
|
|
211
|
+
record.starters.clear();
|
|
191
212
|
record.listeners = {};
|
|
192
213
|
record.migrations = [];
|
|
193
214
|
record.error = err?.message || String(err);
|
|
@@ -17,8 +17,10 @@ export function emptyRecord(source) {
|
|
|
17
17
|
tools: new Map(),
|
|
18
18
|
contexts: [],
|
|
19
19
|
assistantTools: new Map(),
|
|
20
|
+
assistantPages: [],
|
|
20
21
|
publicRoutes: [],
|
|
21
22
|
jobs: new Map(),
|
|
23
|
+
starters: new Map(),
|
|
22
24
|
listeners: {},
|
|
23
25
|
migrations: [],
|
|
24
26
|
disposers: [],
|
|
@@ -83,6 +85,14 @@ export async function pluginToolsOf(tenantId) {
|
|
|
83
85
|
description: tool.description,
|
|
84
86
|
})));
|
|
85
87
|
}
|
|
88
|
+
/** The starters the tenant's plugins bring, in the order the plugins registered them. */
|
|
89
|
+
export async function pluginStartersOf(tenantId) {
|
|
90
|
+
return (await pluginsOf(tenantId)).flatMap((plugin) => [...plugin.starters.values()].map((starter) => ({
|
|
91
|
+
id: `${plugin.source.id}.${starter.id}`,
|
|
92
|
+
plugin,
|
|
93
|
+
starter,
|
|
94
|
+
})));
|
|
95
|
+
}
|
|
86
96
|
/** The built studio half as it is on disk now; null when the plugin has none. */
|
|
87
97
|
export function pluginAsset(record, kind) {
|
|
88
98
|
const file = record.source[kind];
|
|
@@ -9,6 +9,7 @@ import { nanoid } from "nanoid";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { principalOf } from "../auth/index.js";
|
|
11
11
|
import { ConnectError, connectionViews, connectWithCredentials, disconnect, finishOAuth, oauthStateRun, startOAuth, } from "../connectors/index.js";
|
|
12
|
+
import { finishPluginOAuth, pluginStateTenant } from "../connectors/plugin-connections.js";
|
|
12
13
|
import { db, schema, withTenant } from "../db/client.js";
|
|
13
14
|
import { answerAsk, pendingAsk } from "../engine/asks.js";
|
|
14
15
|
import { signalChanged, subscribe } from "../engine/events.js";
|
|
@@ -795,12 +796,19 @@ export const connectCallback = new Hono().get("/callback", async (c) => {
|
|
|
795
796
|
let message = c.req.query("error") === "exchange_failed"
|
|
796
797
|
? "Verbinden hat nicht geklappt. Bitte noch einmal versuchen."
|
|
797
798
|
: "Verbinden wurde abgebrochen.";
|
|
798
|
-
|
|
799
|
+
// A plugin's sign-in (server.connections) names its tenant; a wizard's names its run.
|
|
800
|
+
const pluginTenant = state ? pluginStateTenant(state) : null;
|
|
801
|
+
const tenant = state ? (pluginTenant ?? (await tenantOfRun(oauthStateRun(state) ?? ""))) : null;
|
|
799
802
|
const answer = ticket ? { ticket } : code ? { code } : null;
|
|
800
803
|
if (answer && state && tenant) {
|
|
801
804
|
try {
|
|
802
|
-
|
|
803
|
-
|
|
805
|
+
if (pluginTenant) {
|
|
806
|
+
await withTenant(tenant, () => finishPluginOAuth(answer, state));
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
const done = await withTenant(tenant, () => finishOAuth(answer, state));
|
|
810
|
+
signalChanged(done.runId);
|
|
811
|
+
}
|
|
804
812
|
ok = true;
|
|
805
813
|
message = "Verbunden. Du kannst dieses Fenster schließen.";
|
|
806
814
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { MODEL_CLASSES, TEXT_CLASSES } from "@engenty-wizards/shared/definition";
|
|
3
|
+
import { MODEL_CLASSES, PLUGIN_ID, TEXT_CLASSES } from "@engenty-wizards/shared/definition";
|
|
4
4
|
import { CATEGORY_TYPES, whereSchema } from "@engenty-wizards/shared/knowledge";
|
|
5
5
|
import { PROJECT_FILE_KINDS } from "@engenty-wizards/shared/projects";
|
|
6
6
|
import { isCreditsRef } from "@engenty-wizards/shared/providers";
|
|
@@ -405,6 +405,7 @@ export const studio = new Hono()
|
|
|
405
405
|
.max(24)
|
|
406
406
|
.default([]),
|
|
407
407
|
part: z.enum(["info", "knowledge"]).default("info"),
|
|
408
|
+
plugin: z.string().regex(PLUGIN_ID).optional(),
|
|
408
409
|
})
|
|
409
410
|
.parse(await c.req.json());
|
|
410
411
|
if (!(await canSpend())) {
|
|
@@ -420,6 +421,7 @@ export const studio = new Hono()
|
|
|
420
421
|
message: body.message,
|
|
421
422
|
history: body.history,
|
|
422
423
|
part: body.part,
|
|
424
|
+
plugin: body.plugin,
|
|
423
425
|
signal: abort.signal,
|
|
424
426
|
onText: (delta) => {
|
|
425
427
|
void stream.writeSSE({ event: "text", data: JSON.stringify(delta) });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { statSync } from "node:fs";
|
|
1
3
|
import { DEFINITION_VERSION, readable, wizardSchema, } from "@engenty-wizards/shared/definition";
|
|
2
|
-
import { MARKETPLACE_LANGS, } from "@engenty-wizards/shared/marketplace";
|
|
4
|
+
import { capabilitiesOf, costTierOf, effortOf, formatsOf, MARKETPLACE_LANGS, } from "@engenty-wizards/shared/marketplace";
|
|
3
5
|
import { wizardOutline } from "@engenty-wizards/shared/marketplace-entry";
|
|
4
6
|
import { searchEntries } from "@engenty-wizards/shared/marketplace-search";
|
|
5
7
|
import { and, eq, inArray, notInArray } from "drizzle-orm";
|
|
@@ -9,14 +11,19 @@ import { missingCapabilities, optionalCapabilities, stepsWithoutModel, } from ".
|
|
|
9
11
|
import { env } from "../env.js";
|
|
10
12
|
import { managed } from "../manage.js";
|
|
11
13
|
import { classCatalog, imageCostUsd, tokenCostUsd, videoCostUsd, WEB_SEARCH_COST_USD, } from "../models.js";
|
|
14
|
+
import { pluginStartersOf } from "../plugins/registry.js";
|
|
12
15
|
import { readSetting, writeSetting } from "../settings.js";
|
|
13
|
-
import { LOCAL_TENANT } from "../tenants/tenant.js";
|
|
16
|
+
import { currentTenant, LOCAL_TENANT } from "../tenants/tenant.js";
|
|
14
17
|
import { ServiceError } from "./errors.js";
|
|
15
18
|
/**
|
|
16
19
|
* The marketplace is an app of its own; this runtime is one of its clients
|
|
17
20
|
* (docs/marketplace-contract.md). It searches the marketplace live and starts wizards from its
|
|
18
21
|
* entries. Its starters and what people starred are kept in the control database, so the list
|
|
19
22
|
* and starting a wizard work without a connection too.
|
|
23
|
+
*
|
|
24
|
+
* Beside them stand the starters the tenant's plugins bring (docs/content/dev/plugins/starters.md),
|
|
25
|
+
* listed first and never fetched. They are known as `<plugin id>.<starter id>`; a marketplace
|
|
26
|
+
* entry's id has no dot, so the two never meet.
|
|
20
27
|
*/
|
|
21
28
|
export const asLang = (raw) => MARKETPLACE_LANGS.includes(raw ?? "") ? raw : "de";
|
|
22
29
|
/** A search waits this long for the marketplace; then it searches what is kept here. */
|
|
@@ -84,6 +91,69 @@ function keptItem(e, lang) {
|
|
|
84
91
|
const text = textIn(e, lang);
|
|
85
92
|
return text ? { ...keptSummary(e, lang), definition: text.definition, files: e.files } : null;
|
|
86
93
|
}
|
|
94
|
+
// --- the plugins' starters ------------------------------------------------------------------
|
|
95
|
+
/** One of the starters the tenant's plugins bring, by its id; undefined for any other id. */
|
|
96
|
+
async function pluginStarter(id) {
|
|
97
|
+
if (!id.includes(".")) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
return (await pluginStartersOf(currentTenant())).find((s) => s.id === id);
|
|
101
|
+
}
|
|
102
|
+
/** A plugin's starter as the marketplace lists an entry, read from its definition. */
|
|
103
|
+
function pluginSummary({ id, plugin, starter }) {
|
|
104
|
+
const definition = starter.definition;
|
|
105
|
+
const { name, version } = plugin.source.manifest;
|
|
106
|
+
const capabilities = capabilitiesOf(definition);
|
|
107
|
+
const changed = statSync(plugin.source.entry ?? plugin.source.root, { throwIfNoEntry: false });
|
|
108
|
+
return {
|
|
109
|
+
id,
|
|
110
|
+
revision: 0,
|
|
111
|
+
hash: createHash("sha256")
|
|
112
|
+
.update(`${version}\n${JSON.stringify(definition)}`)
|
|
113
|
+
.digest("hex")
|
|
114
|
+
.slice(0, 16),
|
|
115
|
+
updatedAt: (changed?.mtime ?? new Date()).toISOString(),
|
|
116
|
+
starter: true,
|
|
117
|
+
language: starter.language ?? "de",
|
|
118
|
+
title: starter.title,
|
|
119
|
+
pitch: starter.description,
|
|
120
|
+
// Found by the plugin's name as well: "Termine" finds the appointment plugin's starters.
|
|
121
|
+
terms: [name],
|
|
122
|
+
avatar: definition.avatar,
|
|
123
|
+
formats: formatsOf(definition),
|
|
124
|
+
industries: ["any"],
|
|
125
|
+
useCases: [],
|
|
126
|
+
capabilities,
|
|
127
|
+
effort: effortOf(definition),
|
|
128
|
+
costTier: costTierOf(capabilities),
|
|
129
|
+
steps: definition.steps.length,
|
|
130
|
+
version: definition.version,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** A plugin's starter as this app shows it: never starred, and marked with its plugin. */
|
|
134
|
+
function pluginEntry(found, unavailable) {
|
|
135
|
+
const { id, name } = found.plugin.source.manifest;
|
|
136
|
+
return {
|
|
137
|
+
...asEntry(pluginSummary(found), new Set(), unavailable, optionalOf(found.starter.definition)),
|
|
138
|
+
plugin: { id, name },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** Two searches' counts per option, added up. */
|
|
142
|
+
function addFacets(a, b) {
|
|
143
|
+
const add = (x = {}, y = {}) => {
|
|
144
|
+
const out = { ...x };
|
|
145
|
+
for (const [key, n] of Object.entries(y)) {
|
|
146
|
+
out[key] = (out[key] ?? 0) + n;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
useCase: add(a.useCase, b.useCase),
|
|
152
|
+
industry: add(a.industry, b.industry),
|
|
153
|
+
format: add(a.format, b.format),
|
|
154
|
+
capability: add(a.capability, b.capability),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
87
157
|
// --- searching -----------------------------------------------------------------------------
|
|
88
158
|
/** A summary as this app shows it: whether it can read and run it, and whether it is starred. */
|
|
89
159
|
function asEntry(summary, starred, unavailable, optional = new Set()) {
|
|
@@ -120,8 +190,43 @@ async function keptOptional(ids, lang, unavailable) {
|
|
|
120
190
|
}
|
|
121
191
|
return out;
|
|
122
192
|
}
|
|
123
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* Searches the tenant's plugins' starters and the marketplace, in that order: a page holds the
|
|
195
|
+
* plugins' starters the words and filters find, then the marketplace's entries.
|
|
196
|
+
*/
|
|
124
197
|
export async function searchMarketplace(search) {
|
|
198
|
+
const { q = "", starred, limit = 60, offset = 0, lang: _lang, ...filters } = search;
|
|
199
|
+
const own = starred ? [] : await pluginStartersOf(currentTenant());
|
|
200
|
+
if (!own.length) {
|
|
201
|
+
return searchListed(search);
|
|
202
|
+
}
|
|
203
|
+
const byId = new Map(own.map((s) => [s.id, s]));
|
|
204
|
+
const found = searchEntries(own.map(pluginSummary), q, filters, { limit: own.length });
|
|
205
|
+
const shown = found.entries.slice(offset, offset + limit);
|
|
206
|
+
// The marketplace fills what is left of the page, and counts from where its entries begin.
|
|
207
|
+
const room = limit - shown.length;
|
|
208
|
+
const listed = await searchListed({
|
|
209
|
+
...search,
|
|
210
|
+
limit: Math.max(1, room),
|
|
211
|
+
offset: Math.max(0, offset - found.total),
|
|
212
|
+
});
|
|
213
|
+
const unavailable = new Set(listed.unavailable);
|
|
214
|
+
return {
|
|
215
|
+
...listed,
|
|
216
|
+
entries: [
|
|
217
|
+
...shown.flatMap((e) => {
|
|
218
|
+
const entry = byId.get(e.id);
|
|
219
|
+
return entry ? [pluginEntry(entry, unavailable)] : [];
|
|
220
|
+
}),
|
|
221
|
+
...listed.entries.slice(0, room),
|
|
222
|
+
],
|
|
223
|
+
total: found.total + listed.total,
|
|
224
|
+
all: own.length + listed.all,
|
|
225
|
+
facets: addFacets(listed.facets, found.facets),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/** Searches the marketplace; without an answer, what is kept here, scored the same way. */
|
|
229
|
+
async function searchListed(search) {
|
|
125
230
|
const { q = "", lang, starred: onlyStarred, limit = 60, offset = 0, ...filters } = search;
|
|
126
231
|
const stars = await starredIds();
|
|
127
232
|
const starred = new Set(stars);
|
|
@@ -217,6 +322,15 @@ async function priced(definition) {
|
|
|
217
322
|
}
|
|
218
323
|
/** An entry in a language with its wizard read against this app; null where there is none. */
|
|
219
324
|
export async function marketplaceDetail(id, lang) {
|
|
325
|
+
const own = await pluginStarter(id);
|
|
326
|
+
if (own) {
|
|
327
|
+
const definition = own.starter.definition;
|
|
328
|
+
return {
|
|
329
|
+
...pluginEntry(own, await missingCapabilities()),
|
|
330
|
+
credits: await priced(definition),
|
|
331
|
+
...wizardOutline(definition, Object.keys(own.starter.files ?? {}), lang),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
220
334
|
const found = await fetchItem(id, lang);
|
|
221
335
|
if (!found) {
|
|
222
336
|
return null;
|
|
@@ -242,6 +356,23 @@ export async function marketplaceDetail(id, lang) {
|
|
|
242
356
|
}
|
|
243
357
|
/** An entry's wizard and workspace in a language: what a wizard is made from. */
|
|
244
358
|
export async function marketplaceWizard(id, lang) {
|
|
359
|
+
const own = await pluginStarter(id);
|
|
360
|
+
if (own) {
|
|
361
|
+
// A plugin's starter is in its one language, whatever is asked for.
|
|
362
|
+
const { starter, plugin } = own;
|
|
363
|
+
return {
|
|
364
|
+
id,
|
|
365
|
+
revision: null,
|
|
366
|
+
plugin: plugin.source.id,
|
|
367
|
+
title: starter.title,
|
|
368
|
+
language: starter.language ?? "de",
|
|
369
|
+
definition: structuredClone(starter.definition),
|
|
370
|
+
files: Object.fromEntries(Object.entries(starter.files ?? {}).map(([path, data]) => [
|
|
371
|
+
path,
|
|
372
|
+
typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data),
|
|
373
|
+
])),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
245
376
|
const found = await fetchItem(id, lang);
|
|
246
377
|
if (!found) {
|
|
247
378
|
return null;
|