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.
Files changed (106) hide show
  1. package/apps/runtime/dist/agents/project-assistant.js +10 -1
  2. package/apps/runtime/dist/authoring/guide.js +7 -3
  3. package/apps/runtime/dist/connectors/plugin-connections.js +248 -0
  4. package/apps/runtime/dist/db/migrations/tenant/0010_plugin_connection.sql +15 -0
  5. package/apps/runtime/dist/db/migrations/tenant/meta/0010_snapshot.json +2942 -0
  6. package/apps/runtime/dist/db/migrations/tenant/meta/_journal.json +7 -0
  7. package/apps/runtime/dist/db/schema.js +21 -0
  8. package/apps/runtime/dist/engenty/connections-google/connectors/calendar.js +25 -3
  9. package/apps/runtime/dist/engenty/connections-microsoft/outlook.js +29 -1
  10. package/apps/runtime/dist/engine/input.js +10 -1
  11. package/apps/runtime/dist/mcp/tools.js +3 -2
  12. package/apps/runtime/dist/plugins/loader.js +22 -1
  13. package/apps/runtime/dist/plugins/registry.js +10 -0
  14. package/apps/runtime/dist/routes/runs.js +11 -3
  15. package/apps/runtime/dist/routes/studio.js +3 -1
  16. package/apps/runtime/dist/services/marketplace.js +134 -3
  17. package/apps/runtime/dist/services/runs.js +43 -15
  18. package/apps/runtime/dist/services/wizards.js +4 -2
  19. package/apps/runtime/dist/tools/plugin.js +29 -1
  20. package/apps/web/dist/assets/Account-DW396ig5.js +1 -0
  21. package/apps/web/dist/assets/{ChangelogDialog-CdtQWq0m.js → ChangelogDialog-BdZ1SmEd.js} +1 -1
  22. package/apps/web/dist/assets/CreditsDialog-fOYdoev3.js +1 -0
  23. package/apps/web/dist/assets/EditorPage-CAARFsaI.js +3 -0
  24. package/apps/web/dist/assets/{FlowPreview-IZcNrBJi.js → FlowPreview-U2AEnKlX.js} +1 -1
  25. package/apps/web/dist/assets/{Harness-Cj-k02TA.js → Harness-CtDlm_2b.js} +2 -2
  26. package/apps/web/dist/assets/HomePage-Bd-mkLxW.js +1 -0
  27. package/apps/web/dist/assets/ImportWizard-BMPGjd_t.js +1 -0
  28. package/apps/web/dist/assets/LocalRuntime-DjSNCRnV.js +1 -0
  29. package/apps/web/dist/assets/NewWizardPage-DZPcDR9i.js +2 -0
  30. package/apps/web/dist/assets/ProjectPage-_LAtG3tn.js +2 -0
  31. package/apps/web/dist/assets/PublicRunner-6Iqp0DmU.js +1 -0
  32. package/apps/web/dist/assets/RunDrawer-CtQoY3Ha.js +4 -0
  33. package/apps/web/dist/assets/RunnerView-BLOCNZaZ.js +2 -0
  34. package/apps/web/dist/assets/SettingsPage-Z6skBf6I.js +1 -0
  35. package/apps/web/dist/assets/SetupPage-4-Lic4pT.js +1 -0
  36. package/apps/web/dist/assets/SharePage-DfdhXxZm.js +1 -0
  37. package/apps/web/dist/assets/arrow-right-BOtqnsA1.js +1 -0
  38. package/apps/web/dist/assets/brand-CCAI9p0f.js +2 -0
  39. package/apps/web/dist/assets/changelog-DQM0oz9S.js +1 -0
  40. package/apps/web/dist/assets/circle-alert-BC6xmJ9r.js +1 -0
  41. package/apps/web/dist/assets/definition-D5V79jHA.js +1 -0
  42. package/apps/web/dist/assets/esm-FEWi61F_.js +7 -0
  43. package/apps/web/dist/assets/external-link-wKL1HE6G.js +1 -0
  44. package/apps/web/dist/assets/image-BMdSChOR.js +1 -0
  45. package/apps/web/dist/assets/index-BOp2fz-2.css +1 -0
  46. package/apps/web/dist/assets/index-YJMooz0J.js +132 -0
  47. package/apps/web/dist/assets/lock-BpqzLMuv.js +1 -0
  48. package/apps/web/dist/assets/meta-BqZ7SlZz.js +1 -0
  49. package/apps/web/dist/assets/pen-line-DM-u0dES.js +1 -0
  50. package/apps/web/dist/assets/refresh-cw-Bk7HkOIM.js +1 -0
  51. package/apps/web/dist/assets/settings-menu-BM_NfZfn.js +1 -0
  52. package/apps/web/dist/assets/star-JHzYYl5R.js +1 -0
  53. package/apps/web/dist/assets/trash-CdLg-Ela.js +1 -0
  54. package/apps/web/dist/assets/ui-8QmvuC9u.js +9 -0
  55. package/apps/web/dist/assets/upload-CL1R6shM.js +1 -0
  56. package/apps/web/dist/assets/video-BioLmXNO.js +1 -0
  57. package/apps/web/dist/assets/volume-2-_IOfPNCv.js +1 -0
  58. package/apps/web/dist/assets/wand-sparkles-DYtDQSJZ.js +1 -0
  59. package/apps/web/dist/index.html +4 -5
  60. package/apps/web/dist/mcp/flow.html +57 -57
  61. package/node_modules/@engenty-wizards/plugin-sdk/dist/index.d.ts +85 -0
  62. package/node_modules/@engenty-wizards/plugin-sdk/dist/studio.d.ts +23 -0
  63. package/node_modules/@engenty-wizards/shared/dist/definition.d.ts +28 -1
  64. package/node_modules/@engenty-wizards/shared/dist/definition.js +85 -5
  65. package/node_modules/@engenty-wizards/shared/dist/marketplace.d.ts +5 -0
  66. package/package.json +1 -1
  67. package/apps/web/dist/assets/Account-Cnksw5Ea.js +0 -1
  68. package/apps/web/dist/assets/CreditsDialog-Ti43boCN.js +0 -1
  69. package/apps/web/dist/assets/EditorPage-N7EzlRSj.js +0 -3
  70. package/apps/web/dist/assets/HomePage-bjdKkt84.js +0 -1
  71. package/apps/web/dist/assets/ImportWizard-DagFXYPM.js +0 -1
  72. package/apps/web/dist/assets/LocalRuntime-Boi_zqQJ.js +0 -1
  73. package/apps/web/dist/assets/NewWizardPage-DO5J8OpZ.js +0 -2
  74. package/apps/web/dist/assets/ProjectPage-DdG9wYsd.js +0 -2
  75. package/apps/web/dist/assets/PublicRunner-B4ts9rT6.js +0 -1
  76. package/apps/web/dist/assets/RunDrawer-LnVfCiYO.js +0 -4
  77. package/apps/web/dist/assets/RunnerView-DwffK7AA.js +0 -70
  78. package/apps/web/dist/assets/SettingsPage-Cyx_jTlp.js +0 -1
  79. package/apps/web/dist/assets/SetupPage-CBDD4rpC.js +0 -1
  80. package/apps/web/dist/assets/SharePage-70niRKaO.js +0 -1
  81. package/apps/web/dist/assets/api-C4M6cbpW.js +0 -10
  82. package/apps/web/dist/assets/arrow-right-6FyN3e7D.js +0 -1
  83. package/apps/web/dist/assets/changelog-COyL1B5X.js +0 -1
  84. package/apps/web/dist/assets/check-CUpby_J9.js +0 -1
  85. package/apps/web/dist/assets/chevron-down-DymW-2BP.js +0 -1
  86. package/apps/web/dist/assets/circle-alert-D2SCwFT-.js +0 -1
  87. package/apps/web/dist/assets/createLucideIcon-q3MZMmnf.js +0 -1
  88. package/apps/web/dist/assets/definition-DoCLtZZL.js +0 -55
  89. package/apps/web/dist/assets/esm-CfW8Rmtq.js +0 -7
  90. package/apps/web/dist/assets/external-link-MAL7vJYj.js +0 -1
  91. package/apps/web/dist/assets/image-CC_2mJd6.js +0 -1
  92. package/apps/web/dist/assets/index-CJ0YJ5HC.css +0 -1
  93. package/apps/web/dist/assets/index-DMKXnSf-.js +0 -10
  94. package/apps/web/dist/assets/lock-DZL2N67v.js +0 -1
  95. package/apps/web/dist/assets/meta-6jZygxJq.js +0 -1
  96. package/apps/web/dist/assets/pen-line-NK46C_nG.js +0 -1
  97. package/apps/web/dist/assets/plus-Du-Jx8aI.js +0 -1
  98. package/apps/web/dist/assets/refresh-cw-BNtHis1x.js +0 -1
  99. package/apps/web/dist/assets/settings-menu-Dph4yXbO.js +0 -1
  100. package/apps/web/dist/assets/star-UHmZXpjA.js +0 -1
  101. package/apps/web/dist/assets/trash-0IafyhZB.js +0 -1
  102. package/apps/web/dist/assets/ui-BS348L4M.js +0 -1
  103. package/apps/web/dist/assets/upload-DNRENWzQ.js +0 -1
  104. package/apps/web/dist/assets/video-9fDPRPCv.js +0 -1
  105. package/apps/web/dist/assets/volume-2-DE67_rue.js +0 -1
  106. package/apps/web/dist/assets/wand-sparkles-IEd4jkyn.js +0 -1
@@ -71,6 +71,13 @@
71
71
  "when": 1791311715390,
72
72
  "tag": "0009_knowledge",
73
73
  "breakpoints": true
74
+ },
75
+ {
76
+ "idx": 10,
77
+ "version": "6",
78
+ "when": 1791469862548,
79
+ "tag": "0010_plugin_connection",
80
+ "breakpoints": true
74
81
  }
75
82
  ]
76
83
  }
@@ -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: "The marketplace's ready-made wizards (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.",
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
- const tenant = state ? await tenantOfRun(oauthStateRun(state) ?? "") : null;
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
- const done = await withTenant(tenant, () => finishOAuth(answer, state));
803
- signalChanged(done.runId);
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
- /** Searches the marketplace; without an answer, what is kept here, scored the same way. */
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;