@agent-native/core 0.92.4 → 0.92.6

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 (160) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +19 -0
  3. package/corpus/core/docs/content/template-assets.mdx +139 -0
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/agent/production-agent.ts +96 -6
  6. package/corpus/core/src/cli/design-connect.ts +43 -16
  7. package/corpus/core/src/cli/skills.ts +52 -16
  8. package/corpus/core/src/client/AssistantChat.tsx +127 -15
  9. package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
  10. package/corpus/core/src/client/org/TeamPage.tsx +274 -177
  11. package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
  12. package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
  13. package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
  14. package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
  15. package/corpus/core/src/demo/config.ts +3 -3
  16. package/corpus/core/src/demo/redact.ts +720 -0
  17. package/corpus/core/src/extensions/url-safety.ts +13 -1
  18. package/corpus/core/src/localization/default-messages.ts +7 -0
  19. package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
  20. package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
  21. package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
  22. package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
  23. package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
  24. package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
  25. package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
  26. package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
  27. package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
  28. package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
  29. package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
  30. package/corpus/templates/analytics/AGENTS.md +6 -3
  31. package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
  32. package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
  33. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
  34. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
  35. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
  36. package/corpus/templates/assets/AGENTS.md +13 -0
  37. package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
  38. package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
  39. package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
  40. package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
  41. package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
  42. package/corpus/templates/assets/actions/generate-image.ts +195 -26
  43. package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
  44. package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
  45. package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
  46. package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
  47. package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
  48. package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
  49. package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
  50. package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
  51. package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
  52. package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
  53. package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
  54. package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
  55. package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
  56. package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
  57. package/corpus/templates/assets/app/i18n-data.ts +93 -0
  58. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
  59. package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
  60. package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
  61. package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
  62. package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
  63. package/corpus/templates/assets/server/handlers/assets.ts +7 -27
  64. package/corpus/templates/assets/server/lib/generation.ts +83 -2
  65. package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
  66. package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
  67. package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
  68. package/corpus/templates/assets/shared/api.ts +20 -0
  69. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
  70. package/corpus/templates/calendar/actions/create-event.ts +8 -2
  71. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
  72. package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
  73. package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
  74. package/corpus/templates/calendar/server/handlers/events.ts +9 -2
  75. package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
  76. package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
  77. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
  78. package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
  79. package/corpus/templates/clips/desktop/src/styles.css +13 -9
  80. package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
  81. package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +27 -15
  82. package/corpus/templates/design/actions/connect-localhost.ts +40 -11
  83. package/corpus/templates/design/actions/open-visual-edit.ts +7 -1
  84. package/dist/agent/production-agent.d.ts +34 -1
  85. package/dist/agent/production-agent.d.ts.map +1 -1
  86. package/dist/agent/production-agent.js +58 -5
  87. package/dist/agent/production-agent.js.map +1 -1
  88. package/dist/cli/design-connect.d.ts +5 -1
  89. package/dist/cli/design-connect.d.ts.map +1 -1
  90. package/dist/cli/design-connect.js +37 -16
  91. package/dist/cli/design-connect.js.map +1 -1
  92. package/dist/cli/skills.d.ts.map +1 -1
  93. package/dist/cli/skills.js +52 -16
  94. package/dist/cli/skills.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts +3 -1
  96. package/dist/client/AssistantChat.d.ts.map +1 -1
  97. package/dist/client/AssistantChat.js +96 -15
  98. package/dist/client/AssistantChat.js.map +1 -1
  99. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  100. package/dist/client/chat/tool-call-display.js +5 -1
  101. package/dist/client/chat/tool-call-display.js.map +1 -1
  102. package/dist/client/org/TeamPage.d.ts.map +1 -1
  103. package/dist/client/org/TeamPage.js +32 -13
  104. package/dist/client/org/TeamPage.js.map +1 -1
  105. package/dist/client/settings/DemoModeSection.d.ts +3 -3
  106. package/dist/client/settings/DemoModeSection.js +4 -4
  107. package/dist/client/settings/DemoModeSection.js.map +1 -1
  108. package/dist/client/settings/SettingsPanel.js +1 -1
  109. package/dist/client/settings/SettingsPanel.js.map +1 -1
  110. package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
  111. package/dist/client/settings/SettingsTabsPage.js +38 -2
  112. package/dist/client/settings/SettingsTabsPage.js.map +1 -1
  113. package/dist/collab/routes.d.ts +1 -1
  114. package/dist/collab/struct-routes.d.ts +1 -1
  115. package/dist/demo/actions/toggle-demo-mode.js +2 -2
  116. package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
  117. package/dist/demo/config.js +3 -3
  118. package/dist/demo/config.js.map +1 -1
  119. package/dist/demo/redact.d.ts +1 -1
  120. package/dist/demo/redact.d.ts.map +1 -1
  121. package/dist/demo/redact.js +0 -0
  122. package/dist/demo/redact.js.map +1 -1
  123. package/dist/extensions/url-safety.d.ts +5 -0
  124. package/dist/extensions/url-safety.d.ts.map +1 -1
  125. package/dist/extensions/url-safety.js +10 -0
  126. package/dist/extensions/url-safety.js.map +1 -1
  127. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  128. package/dist/localization/default-messages.d.ts +7 -0
  129. package/dist/localization/default-messages.d.ts.map +1 -1
  130. package/dist/localization/default-messages.js +7 -0
  131. package/dist/localization/default-messages.js.map +1 -1
  132. package/dist/notifications/routes.d.ts +2 -2
  133. package/dist/observability/routes.d.ts +3 -3
  134. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  135. package/dist/resources/handlers.d.ts +1 -1
  136. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  137. package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
  138. package/dist/templates/default/app/i18n/de-DE.ts +7 -0
  139. package/dist/templates/default/app/i18n/en-US.ts +7 -0
  140. package/dist/templates/default/app/i18n/es-ES.ts +7 -0
  141. package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
  142. package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
  143. package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
  144. package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
  145. package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
  146. package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
  147. package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
  148. package/docs/content/template-assets.mdx +139 -0
  149. package/package.json +1 -1
  150. package/src/templates/default/app/i18n/ar-SA.ts +7 -0
  151. package/src/templates/default/app/i18n/de-DE.ts +7 -0
  152. package/src/templates/default/app/i18n/en-US.ts +7 -0
  153. package/src/templates/default/app/i18n/es-ES.ts +7 -0
  154. package/src/templates/default/app/i18n/fr-FR.ts +7 -0
  155. package/src/templates/default/app/i18n/hi-IN.ts +7 -0
  156. package/src/templates/default/app/i18n/ja-JP.ts +7 -0
  157. package/src/templates/default/app/i18n/ko-KR.ts +7 -0
  158. package/src/templates/default/app/i18n/pt-BR.ts +7 -0
  159. package/src/templates/default/app/i18n/zh-CN.ts +7 -0
  160. package/src/templates/default/app/i18n/zh-TW.ts +7 -0
@@ -247,17 +247,26 @@ export async function createSsrfSafeDispatcher(): Promise<unknown | null> {
247
247
  * Throws an Error whose message starts with "SSRF blocked:" when a target
248
248
  * (initial or via redirect) resolves to a private/internal address, or when the
249
249
  * redirect limit is exceeded. Otherwise returns the final Response.
250
+ *
251
+ * `httpsOnly` extends the per-hop validation to the URL scheme: redirects are
252
+ * followed only to `https:` targets, so an HTTPS-only caller cannot be
253
+ * downgraded to plain HTTP by a 30x from the (untrusted) origin.
250
254
  */
251
255
  export async function ssrfSafeFetch(
252
256
  url: string,
253
257
  init: RequestInit = {},
254
- options: { maxRedirects?: number } = {},
258
+ options: { maxRedirects?: number; httpsOnly?: boolean } = {},
255
259
  ): Promise<Response> {
256
260
  const maxRedirects = options.maxRedirects ?? 3;
257
261
  const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
258
262
 
259
263
  let currentUrl = url;
260
264
  for (let hop = 0; hop <= maxRedirects; hop++) {
265
+ if (options.httpsOnly && new URL(currentUrl).protocol !== "https:") {
266
+ throw new Error(
267
+ `SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`,
268
+ );
269
+ }
261
270
  if (await isBlockedExtensionUrlWithDns(currentUrl)) {
262
271
  throw new Error(
263
272
  `SSRF blocked: refusing to fetch private/internal address (${currentUrl})`,
@@ -273,6 +282,9 @@ export async function ssrfSafeFetch(
273
282
  if (response.status >= 300 && response.status < 400) {
274
283
  const location = response.headers.get("location");
275
284
  if (!location) return response;
285
+ // Drain the redirect body so the hop's connection is released instead
286
+ // of being held until GC.
287
+ await response.body?.cancel().catch(() => {});
276
288
  currentUrl = new URL(location, currentUrl).href;
277
289
  continue;
278
290
  }
@@ -410,9 +410,16 @@ const messages = {
410
410
  join: "Join",
411
411
  invitedByLabel: "Invited by {{name}}",
412
412
  you: "You",
413
+ owner: "Owner",
413
414
  admin: "Admin",
414
415
  member: "Member",
415
416
  members: "Members",
417
+ inviteMembers: "Invite members",
418
+ invited: "Invited",
419
+ role: "Role",
420
+ status: "Status",
421
+ actions: "Actions",
422
+ noMembers: "No members yet.",
416
423
  memberCount_one: "{{count}} member",
417
424
  memberCount_other: "{{count}} members",
418
425
  youAreRole: "You are {{role}}",
@@ -423,9 +423,16 @@ const messages = {
423
423
  join: "Join",
424
424
  invitedByLabel: "Invited by {{name}}",
425
425
  you: "You",
426
+ owner: "المالك",
426
427
  admin: "مسؤول",
427
428
  member: "Member",
428
429
  members: "Members",
430
+ inviteMembers: "دعوة أعضاء",
431
+ invited: "تمت الدعوة",
432
+ role: "الدور",
433
+ status: "الحالة",
434
+ actions: "الإجراءات",
435
+ noMembers: "لا يوجد أعضاء بعد.",
429
436
  memberCount_one: "{{count}} member",
430
437
  memberCount_other: "{{count}} members",
431
438
  memberCount_zero: "{{count}} members",
@@ -410,9 +410,16 @@ const messages = {
410
410
  join: "Join",
411
411
  invitedByLabel: "Invited by {{name}}",
412
412
  you: "You",
413
+ owner: "Eigentümer",
413
414
  admin: "Admin",
414
415
  member: "Member",
415
416
  members: "Members",
417
+ inviteMembers: "Mitglieder einladen",
418
+ invited: "Eingeladen",
419
+ role: "Rolle",
420
+ status: "Status",
421
+ actions: "Aktionen",
422
+ noMembers: "Noch keine Mitglieder.",
416
423
  memberCount_one: "{{count}} member",
417
424
  memberCount_other: "{{count}} members",
418
425
  youAreRole: "You are {{role}}",
@@ -401,9 +401,16 @@ const messages = {
401
401
  join: "Join",
402
402
  invitedByLabel: "Invited by {{name}}",
403
403
  you: "You",
404
+ owner: "Owner",
404
405
  admin: "Admin",
405
406
  member: "Member",
406
407
  members: "Members",
408
+ inviteMembers: "Invite members",
409
+ invited: "Invited",
410
+ role: "Role",
411
+ status: "Status",
412
+ actions: "Actions",
413
+ noMembers: "No members yet.",
407
414
  memberCount_one: "{{count}} member",
408
415
  memberCount_other: "{{count}} members",
409
416
  youAreRole: "You are {{role}}",
@@ -417,9 +417,16 @@ const messages = {
417
417
  join: "Join",
418
418
  invitedByLabel: "Invited by {{name}}",
419
419
  you: "You",
420
+ owner: "Propietario",
420
421
  admin: "Administrador",
421
422
  member: "Member",
422
423
  members: "Members",
424
+ inviteMembers: "Invitar miembros",
425
+ invited: "Invitado",
426
+ role: "Rol",
427
+ status: "Estado",
428
+ actions: "Acciones",
429
+ noMembers: "Aún no hay miembros.",
423
430
  memberCount_one: "{{count}} member",
424
431
  memberCount_other: "{{count}} members",
425
432
  memberCount_many: "{{count}} members",
@@ -415,9 +415,16 @@ const messages = {
415
415
  join: "Join",
416
416
  invitedByLabel: "Invited by {{name}}",
417
417
  you: "You",
418
+ owner: "Propriétaire",
418
419
  admin: "Admin",
419
420
  member: "Member",
420
421
  members: "Members",
422
+ inviteMembers: "Inviter des membres",
423
+ invited: "Invité",
424
+ role: "Rôle",
425
+ status: "Statut",
426
+ actions: "Actions",
427
+ noMembers: "Aucun membre pour le moment.",
421
428
  memberCount_one: "{{count}} member",
422
429
  memberCount_other: "{{count}} members",
423
430
  memberCount_many: "{{count}} members",
@@ -397,9 +397,16 @@ const messages = {
397
397
  join: "Join",
398
398
  invitedByLabel: "Invited by {{name}}",
399
399
  you: "You",
400
+ owner: "स्वामी",
400
401
  admin: "व्यवस्थापक",
401
402
  member: "Member",
402
403
  members: "Members",
404
+ inviteMembers: "सदस्यों को आमंत्रित करें",
405
+ invited: "आमंत्रित",
406
+ role: "भूमिका",
407
+ status: "स्थिति",
408
+ actions: "कार्रवाइयां",
409
+ noMembers: "अभी कोई सदस्य नहीं है.",
403
410
  memberCount_one: "{{count}} member",
404
411
  memberCount_other: "{{count}} members",
405
412
  youAreRole: "You are {{role}}",
@@ -402,9 +402,16 @@ const messages = {
402
402
  join: "Join",
403
403
  invitedByLabel: "Invited by {{name}}",
404
404
  you: "You",
405
+ owner: "オーナー",
405
406
  admin: "管理者",
406
407
  member: "Member",
407
408
  members: "Members",
409
+ inviteMembers: "メンバーを招待",
410
+ invited: "招待済み",
411
+ role: "ロール",
412
+ status: "ステータス",
413
+ actions: "操作",
414
+ noMembers: "メンバーはまだいません。",
408
415
  memberCount_other: "{{count}} members",
409
416
  youAreRole: "You are {{role}}",
410
417
  changeRole: "役割を変更する",
@@ -395,9 +395,16 @@ const messages = {
395
395
  join: "Join",
396
396
  invitedByLabel: "Invited by {{name}}",
397
397
  you: "You",
398
+ owner: "소유자",
398
399
  admin: "관리자",
399
400
  member: "Member",
400
401
  members: "Members",
402
+ inviteMembers: "구성원 초대",
403
+ invited: "초대됨",
404
+ role: "역할",
405
+ status: "상태",
406
+ actions: "작업",
407
+ noMembers: "아직 구성원이 없습니다.",
401
408
  memberCount_other: "{{count}} members",
402
409
  youAreRole: "You are {{role}}",
403
410
  changeRole: "역할 변경",
@@ -414,9 +414,16 @@ const messages = {
414
414
  join: "Join",
415
415
  invitedByLabel: "Invited by {{name}}",
416
416
  you: "You",
417
+ owner: "Proprietário",
417
418
  admin: "Admin",
418
419
  member: "Member",
419
420
  members: "Members",
421
+ inviteMembers: "Convidar membros",
422
+ invited: "Convidado",
423
+ role: "Função",
424
+ status: "Status",
425
+ actions: "Ações",
426
+ noMembers: "Ainda não há membros.",
420
427
  memberCount_one: "{{count}} member",
421
428
  memberCount_other: "{{count}} members",
422
429
  memberCount_many: "{{count}} members",
@@ -383,9 +383,16 @@ const messages = {
383
383
  join: "Join",
384
384
  invitedByLabel: "Invited by {{name}}",
385
385
  you: "You",
386
+ owner: "所有者",
386
387
  admin: "管理员",
387
388
  member: "Member",
388
389
  members: "Members",
390
+ inviteMembers: "邀请成员",
391
+ invited: "已邀请",
392
+ role: "角色",
393
+ status: "状态",
394
+ actions: "操作",
395
+ noMembers: "暂无成员。",
389
396
  memberCount_other: "{{count}} members",
390
397
  youAreRole: "You are {{role}}",
391
398
  changeRole: "改变角色",
@@ -381,9 +381,16 @@ const messages = {
381
381
  join: "Join",
382
382
  invitedByLabel: "由 {{name}} 邀請",
383
383
  you: "You",
384
+ owner: "擁有者",
384
385
  admin: "管理員",
385
386
  member: "Member",
386
387
  members: "Members",
388
+ inviteMembers: "邀請成員",
389
+ invited: "已邀請",
390
+ role: "角色",
391
+ status: "狀態",
392
+ actions: "操作",
393
+ noMembers: "尚無成員。",
387
394
  memberCount_other: "{{count}} 位成員",
388
395
  youAreRole: "你的角色是 {{role}}",
389
396
  changeRole: "改變角色",
@@ -236,9 +236,12 @@ details live in `.agents/skills/`.
236
236
  `run-monitor-check`, `delete-monitor`. Checks/alerting run server-side in
237
237
  `server/lib/uptime-monitors.ts` (sweep job `server/jobs/uptime-monitors.ts`,
238
238
  scheduler `server/plugins/uptime-monitor-jobs.ts`) over
239
- `server/db/schema-monitoring.ts`. Deep links: list `?view=uptime`, detail
240
- `?view=uptime&monitor=<id>`, create `?view=uptime&monitor=new`, edit
241
- `?view=uptime&monitor=<id>&edit=1`. See `docs/uptime-monitoring.md`.
239
+ `server/db/schema-monitoring.ts`. Production serverless/Netlify-style
240
+ runtimes skip the in-process interval scheduler and rely on the generated
241
+ scheduled/background worker or external cron instead. Deep links: list
242
+ `?view=uptime`, detail `?view=uptime&monitor=<id>`, create
243
+ `?view=uptime&monitor=new`, edit `?view=uptime&monitor=<id>&edit=1`. See
244
+ `docs/uptime-monitoring.md`.
242
245
  - Status pages (`app/pages/monitoring/uptime/status-pages/**`) are a config
243
246
  sub-view under Uptime that bundle chosen monitors under a public
244
247
  `/status/<slug>` page. Actions: `list-status-pages`, `get-status-page`,
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-09
4
+ ---
5
+
6
+ Uptime monitors now skip in-process sweeps in production serverless runtimes and rely on scheduled workers to avoid false timeout alerts.
@@ -122,6 +122,11 @@ external cron with `UPTIME_MONITORS_CRON_SECRET`. Both paths run
122
122
  monitors whose interval has elapsed, checks them, records results, opens/resolves
123
123
  incidents, and prunes old check results.
124
124
 
125
+ Production serverless function runtimes skip the in-process interval scheduler
126
+ even when `UPTIME_MONITOR_JOBS=1`, because the function may freeze or time out
127
+ mid-sweep. Use the generated scheduled/background worker or an external cron
128
+ calling `/api/uptime-monitors/run` for production serverless checks.
129
+
125
130
  Environment flags (mirrors the analytics alert job):
126
131
 
127
132
  | flag | effect |
@@ -16,9 +16,21 @@ declare global {
16
16
  var __AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__: boolean | undefined;
17
17
  }
18
18
 
19
- function platformSchedulerOwnsMonitors(): boolean {
19
+ function isProductionServerlessRuntime(): boolean {
20
+ if (process.env.NODE_ENV !== "production") return false;
20
21
  return (
21
22
  process.env.NETLIFY === "true" ||
23
+ Boolean(process.env.NETLIFY_FUNCTION_NAME) ||
24
+ Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME) ||
25
+ Boolean(process.env.LAMBDA_TASK_ROOT) ||
26
+ process.env.AWS_EXECUTION_ENV?.startsWith("AWS_Lambda") === true ||
27
+ process.env.VERCEL === "1"
28
+ );
29
+ }
30
+
31
+ function platformSchedulerOwnsMonitors(): boolean {
32
+ return (
33
+ isProductionServerlessRuntime() ||
22
34
  globalThis.__AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__ === true
23
35
  );
24
36
  }
@@ -44,7 +56,7 @@ export default function registerUptimeMonitorJobs(): void {
44
56
  if (!skippingLogged) {
45
57
  console.log(
46
58
  platformSchedulerOwnsMonitors()
47
- ? "[uptime-monitors] Skipping in-process cron because the platform scheduler owns monitor sweeps."
59
+ ? "[uptime-monitors] Skipping in-process cron because production serverless runtimes rely on scheduled/background monitor sweeps."
48
60
  : "[uptime-monitors] Skipping background cron (set UPTIME_MONITOR_JOBS=1 or RUN_BACKGROUND_JOBS=1 to enable in dev; on by default in production)",
49
61
  );
50
62
  skippingLogged = true;
@@ -27,6 +27,9 @@ Use this skill before calling `generate-image`, `generate-image-batch`, or
27
27
  - Use category-tagged references. Blog heroes should prefer `hero`; diagrams
28
28
  should prefer `diagram`; product imagery should include `product` and `logo`
29
29
  references.
30
+ - Imported external images with `status: "reference"` are valid generation
31
+ inputs. Use their returned asset IDs in preset reference fills or reference
32
+ boards the same way you would use uploaded reference assets.
30
33
  - Keep reference sets small and deterministic. Prefer anchors listed in
31
34
  `assetLibraries.settings.canonicalStyleAssetIds` and assets marked
32
35
  `assets.metadata.isStyleAnchor` before sampling other relevant references.
@@ -59,12 +62,44 @@ Use this skill before calling `generate-image`, `generate-image-batch`, or
59
62
  `generate-image-batch` / `refine-image`. The design team uses the audit log
60
63
  to review quality by app, library, model, prompt, and lineage.
61
64
 
65
+ ## Preset-first Generation
66
+
67
+ - Before any ad-hoc generation for a brand kit, call
68
+ `list-generation-presets` and scan titles/descriptions/categories for a
69
+ use-case match. A preset encodes the designer's format, model, layout, and
70
+ reference board; using it is always better than improvising.
71
+ - If one preset clearly matches: use its `presetId`; do not restate its saved
72
+ aspect ratio/size/model/tier. If several plausibly match: pick the best and
73
+ state which one you used; do not ask the user to choose.
74
+ - Match named people/products/backdrops in the request to the preset's
75
+ reference board entry labels in `settings.presetReferences`. Fill required
76
+ variable entries via `presetReferenceFills`: search the library for assets of
77
+ those people first; ask the user for photos only when none exist. Never skip a
78
+ required entry.
79
+ - Route exact visible copy such as event titles, dates, and times to
80
+ `embeddedText` per the existing text rules; keep the creative direction in
81
+ `prompt`.
82
+ - If nothing matches: generate ad-hoc, say that no preset fit, and mention a
83
+ preset could be created for this recurring use case.
84
+
62
85
  ## Prompting
63
86
 
64
87
  - Treat references as evidence, not decoration.
65
88
  - Let the server choose references unless the user named exact assets. Automatic
66
89
  generation uses up to 6 relevant current references, seeded by canonical
67
90
  style anchors; explicit `referenceAssetIds` are preserved.
91
+ - Preset reference boards live on tagged presets as named entries such as a
92
+ usual host, product, backdrop, style sample, or per-event speaker. Fixed
93
+ entries attach automatically. Variable entries may be replaced for a run with
94
+ `presetReferenceFills`; each fill REPLACES that entry's pinned images rather
95
+ than appending. Required variable entries block generation until you provide
96
+ at least one image.
97
+ - When a tagged preset brief names required variable references, collect the
98
+ needed images from the user's attachments or the library and pass
99
+ `presetReferenceFills: [{ referenceId, assetIds }]` to `generate-image` or
100
+ `generate-image-batch`. Board images are additive to brand style references.
101
+ User-uploaded per-event people/photos should be uploaded as content images
102
+ (`subject` intent/role), not as reusable brand style references.
68
103
  - If a collection's style feels underspecified, call `analyze-collection-style`
69
104
  and use its vision brand analysis for palette, composition, lighting, subject
70
105
  treatment, typography policy, and constraints.
@@ -90,6 +90,25 @@ Reference images and generated images live in the **same** `image_assets` table,
90
90
 
91
91
  The unified table simplifies access control (one `library_id`, one access check) and makes "use a saved generation as a reference for a future generation" a first-class operation — just bump its `role` to `prior-candidate` (planned for v2; v1 just selects from any non-archived asset).
92
92
 
93
+ ## Importing external references
94
+
95
+ Use `import-asset-from-url` when the agent has found a public HTTPS image that
96
+ belongs in a brand kit, such as a blog hero, product shot, logo, campaign image,
97
+ or diagram. Choose the narrowest reference `role` (`style_reference`,
98
+ `subject_reference`, `product_reference`, `background_reference`,
99
+ `logo_reference`, or `diagram_reference`) and preserve a useful title or
100
+ description when known. The deliverable `category` defaults to match the role
101
+ (logo → `logo`, product → `product`, diagram → `diagram`); pass an explicit
102
+ `category` such as `hero` or `campaign` when the image belongs in one of those
103
+ filtered views.
104
+
105
+ For a blog-to-brand-kit workflow: inspect the page, pick the strongest image
106
+ URLs, import each URL into the target `libraryId`, then wire the returned
107
+ `assetId`s into generation preset reference fills or call `set-canonical-logo`
108
+ for the exact logo. Imported assets are stored as `status: "reference"` with
109
+ `sourceUrl` provenance, so downstream generation, preset boards, and logo
110
+ compositing can use them like uploaded reference assets.
111
+
93
112
  ## When to add a collection
94
113
 
95
114
  Collections are optional. Most users won't create them. Use them when:
@@ -24,6 +24,8 @@ Detailed library, generation, image, embed, and engine rules live in
24
24
  - Use the configured generation/engine path for image and asset work. Do not add
25
25
  ad hoc provider calls when the app has an action/engine abstraction.
26
26
  - Preserve provenance and metadata for generated or imported assets.
27
+ - Ingest external brand or blog imagery with `import-asset-from-url`, then pin
28
+ the returned asset to preset reference boards or set it as the canonical logo.
27
29
  - Use `view-screen` when the active library, selected asset, picker, generation,
28
30
  or embed target is unclear. The human Library surface is `/library` for
29
31
  cross-kit browsing and `/library/:libraryId` for a single brand kit; embedded
@@ -59,6 +61,17 @@ prompt, aspectRatio }`.
59
61
  composition, mood, lighting, and subject — then pass the `presetId` to
60
62
  `generate-image` / `generate-image-batch` so the saved format/model/tier/logo
61
63
  apply automatically. Do not restate those as ad-hoc args.
64
+ - Image requests without a tagged preset are preset-first: the user may not know
65
+ presets exist. Call `list-generation-presets` for the library before
66
+ generating and compare the request against each preset's title, description,
67
+ and category. If one matches the use case (e.g. "livestream poster" -> a
68
+ Livestream Announcement preset), generate with its `presetId` instead of
69
+ ad-hoc settings. Only generate presetless when nothing plausibly matches.
70
+ - Presets may declare a reference board with fixed or variable named entries
71
+ such as a host, guest speaker, product, backdrop, or style. Fixed entries
72
+ attach automatically; collect images for required variable entries and pass
73
+ them via `presetReferenceFills` on `generate-image` /
74
+ `generate-image-batch`. See the `image-generation` skill for fill semantics.
62
75
  - For exact visible copy inside a generated image, pass `embeddedText` and
63
76
  optional `textPlacement` to `generate-image` or each `generate-image-batch`
64
77
  slot. Keep the general `prompt` for creative direction; the structured text
@@ -1,5 +1,11 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import {
4
+ PRESET_REFERENCE_SUBJECT_IMAGES_ERROR,
5
+ PRESET_REFERENCE_TOTAL_IMAGES_ERROR,
6
+ } from "../server/lib/preset-references.js";
7
+ import { PRESET_REFERENCE_ROLES } from "../shared/api.js";
8
+
3
9
  const fractionSchema = z.coerce.number().min(0).max(1);
4
10
  const positiveFractionSchema = z.coerce.number().min(0.02).max(1);
5
11
 
@@ -40,9 +46,76 @@ export const presetSkeletonSpecSchema = z.object({
40
46
  .optional(),
41
47
  });
42
48
 
49
+ export const presetReferenceSchema = z.object({
50
+ id: z
51
+ .string()
52
+ .regex(/^[a-z0-9][a-z0-9-]*$/)
53
+ .max(40),
54
+ label: z.string().min(1).max(60),
55
+ role: z.enum(PRESET_REFERENCE_ROLES),
56
+ description: z.string().max(400).optional(),
57
+ assetIds: z.array(z.string().min(1)).max(4),
58
+ variable: z.coerce.boolean(),
59
+ required: z.coerce.boolean(),
60
+ });
61
+
43
62
  export const generationPresetSettingsSchema = z
44
63
  .object({
45
64
  includeLogo: z.coerce.boolean().optional(),
46
65
  skeletonSpec: presetSkeletonSpecSchema.nullable().optional(),
66
+ presetReferences: z
67
+ .array(presetReferenceSchema)
68
+ .max(6)
69
+ .nullable()
70
+ .optional(),
71
+ })
72
+ .superRefine((settings, ctx) => {
73
+ const references = settings.presetReferences ?? [];
74
+ const seen = new Set<string>();
75
+ for (const entry of references) {
76
+ if (seen.has(entry.id)) {
77
+ ctx.addIssue({
78
+ code: "custom",
79
+ path: ["presetReferences"],
80
+ message: "Reference entry ids must be unique.",
81
+ });
82
+ break;
83
+ }
84
+ seen.add(entry.id);
85
+ if (entry.required && !entry.variable && entry.assetIds.length === 0) {
86
+ ctx.addIssue({
87
+ code: "custom",
88
+ path: ["presetReferences", entry.id],
89
+ message:
90
+ "A required fixed reference needs at least one image. Pin images or mark it as variable.",
91
+ });
92
+ }
93
+ }
94
+ // Budget with the minimum images each entry consumes at run time: a
95
+ // required entry with no pinned images still needs at least one fill
96
+ // image, so reserve it here or the saved preset can never generate.
97
+ const minimumImages = (entry: { required: boolean; assetIds: string[] }) =>
98
+ Math.max(entry.assetIds.length, entry.required ? 1 : 0);
99
+ const total = references.reduce(
100
+ (sum, entry) => sum + minimumImages(entry),
101
+ 0,
102
+ );
103
+ if (total > 8) {
104
+ ctx.addIssue({
105
+ code: "custom",
106
+ path: ["presetReferences"],
107
+ message: PRESET_REFERENCE_TOTAL_IMAGES_ERROR,
108
+ });
109
+ }
110
+ const subjectTotal = references
111
+ .filter((entry) => entry.role === "subject")
112
+ .reduce((sum, entry) => sum + minimumImages(entry), 0);
113
+ if (subjectTotal > 4) {
114
+ ctx.addIssue({
115
+ code: "custom",
116
+ path: ["presetReferences"],
117
+ message: PRESET_REFERENCE_SUBJECT_IMAGES_ERROR,
118
+ });
119
+ }
47
120
  })
48
121
  .catchall(z.unknown());
@@ -1,6 +1,7 @@
1
1
  import { inArray } from "drizzle-orm";
2
2
 
3
3
  import { schema } from "../server/db/index.js";
4
+ import { normalizePresetReferences } from "../server/lib/preset-references.js";
4
5
 
5
6
  export async function assertPresetSkeletonAssetsValid(input: {
6
7
  db: any;
@@ -62,3 +63,65 @@ export async function assertPresetSkeletonAssetsValid(input: {
62
63
  );
63
64
  }
64
65
  }
66
+
67
+ export async function assertPresetReferenceAssetsValid(input: {
68
+ db: any;
69
+ libraryId: string;
70
+ settings: unknown;
71
+ }) {
72
+ const rawReferences =
73
+ input.settings && typeof input.settings === "object"
74
+ ? (input.settings as { presetReferences?: unknown }).presetReferences
75
+ : null;
76
+ const references = normalizePresetReferences(rawReferences);
77
+ const assetIds = [...new Set(references.flatMap((entry) => entry.assetIds))];
78
+ if (!assetIds.length) return;
79
+
80
+ const rows = await input.db
81
+ .select({
82
+ id: schema.assets.id,
83
+ libraryId: schema.assets.libraryId,
84
+ mimeType: schema.assets.mimeType,
85
+ status: schema.assets.status,
86
+ })
87
+ .from(schema.assets)
88
+ .where(inArray(schema.assets.id, assetIds));
89
+ const byId = new Map<string, any>(
90
+ (rows as any[]).map((asset) => [asset.id, asset]),
91
+ );
92
+ const valid = assetIds.every((assetId) => {
93
+ const asset = byId.get(assetId);
94
+ return (
95
+ asset &&
96
+ asset.libraryId === input.libraryId &&
97
+ typeof asset.mimeType === "string" &&
98
+ asset.mimeType.startsWith("image/") &&
99
+ asset.status !== "archived" &&
100
+ asset.status !== "failed"
101
+ );
102
+ });
103
+ if (!valid) {
104
+ throw new Error(
105
+ "Reference board images must be images in this asset library.",
106
+ );
107
+ }
108
+ }
109
+
110
+ export function assertPresetReferenceModelCompatible(input: {
111
+ model: string | null | undefined;
112
+ settings: unknown;
113
+ }) {
114
+ const rawReferences =
115
+ input.settings && typeof input.settings === "object"
116
+ ? (input.settings as { presetReferences?: unknown }).presetReferences
117
+ : null;
118
+ const references = normalizePresetReferences(rawReferences);
119
+ if (
120
+ input.model === "gemini-2.5-flash-image" &&
121
+ references.some((entry) => entry.role === "subject")
122
+ ) {
123
+ throw new Error(
124
+ "Subject reference entries need a model with character consistency. Use gemini-3.1-flash-image, gemini-3-pro-image, gpt-image-1, or gpt-image-2.",
125
+ );
126
+ }
127
+ }
@@ -15,7 +15,11 @@ import {
15
15
  } from "../shared/api.js";
16
16
  import { generationPresetSettingsSchema } from "./_generation-preset-settings.js";
17
17
  import { serializeGenerationPreset } from "./_helpers.js";
18
- import { assertPresetSkeletonAssetsValid } from "./_preset-skeleton-validation.js";
18
+ import {
19
+ assertPresetReferenceAssetsValid,
20
+ assertPresetReferenceModelCompatible,
21
+ assertPresetSkeletonAssetsValid,
22
+ } from "./_preset-skeleton-validation.js";
19
23
 
20
24
  export default defineAction({
21
25
  description:
@@ -56,10 +60,25 @@ export default defineAction({
56
60
  throw new Error("Collection does not belong to this asset library.");
57
61
  }
58
62
  }
63
+ const settings = {
64
+ ...(args.settings ?? {}),
65
+ ...(args.includeLogo !== undefined
66
+ ? { includeLogo: args.includeLogo }
67
+ : {}),
68
+ };
59
69
  await assertPresetSkeletonAssetsValid({
60
70
  db,
61
71
  libraryId: args.libraryId,
62
- settings: args.settings,
72
+ settings,
73
+ });
74
+ await assertPresetReferenceAssetsValid({
75
+ db,
76
+ libraryId: args.libraryId,
77
+ settings,
78
+ });
79
+ assertPresetReferenceModelCompatible({
80
+ model: args.model,
81
+ settings,
63
82
  });
64
83
  const now = nowIso();
65
84
  const row = {
@@ -76,12 +95,7 @@ export default defineAction({
76
95
  model: args.model,
77
96
  textPolicy: args.textPolicy,
78
97
  referencePolicy: args.referencePolicy,
79
- settings: stringifyJson({
80
- ...(args.settings ?? {}),
81
- ...(args.includeLogo !== undefined
82
- ? { includeLogo: args.includeLogo }
83
- : {}),
84
- }),
98
+ settings: stringifyJson(settings),
85
99
  sortOrder: args.sortOrder ?? 100,
86
100
  createdAt: now,
87
101
  updatedAt: now,