@agent-native/core 0.157.0 → 0.157.2

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 (114) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/global.css +4 -0
  5. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
  6. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  7. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  8. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  9. package/corpus/templates/calendar/server/handlers/bookings.ts +224 -93
  10. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  11. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  12. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  13. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  14. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  15. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/request-recording-access.ts +305 -0
  17. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  18. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
  20. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  21. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  22. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  23. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  24. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
  25. package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
  26. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  27. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  28. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  29. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  30. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  31. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  32. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  33. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  34. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  35. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  36. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  37. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  38. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  39. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  40. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  41. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  42. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  43. package/corpus/templates/clips/server/db/index.ts +1 -0
  44. package/corpus/templates/clips/server/db/schema.ts +1 -0
  45. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  46. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  47. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  48. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  49. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  50. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  51. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  52. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  53. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  55. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +133 -4
  58. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  59. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  60. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  61. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  62. package/corpus/templates/factory/AGENTS.md +5 -1
  63. package/corpus/templates/factory/README.md +6 -2
  64. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  65. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  66. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  67. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  68. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  69. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  70. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  71. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  72. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  73. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  74. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  75. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  76. package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
  77. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  78. package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
  79. package/dist/client/AgentPanel.d.ts +13 -1
  80. package/dist/client/AgentPanel.js +12 -10
  81. package/dist/client/AssistantChat.js +25 -5
  82. package/dist/client/HighlightedCodeBlock.js +12 -2
  83. package/dist/client/agent-chat/index.d.ts +1 -0
  84. package/dist/client/agent-chat/index.js +1 -0
  85. package/dist/client/chat/markdown-renderer.js +16 -3
  86. package/dist/client/chat-first/apps-rail.js +1 -1
  87. package/dist/client/components/ui/message-scroller.js +1 -1
  88. package/dist/client/settings/AccountSettingsCard.js +48 -19
  89. package/dist/client/sharing/ShareButton.d.ts +1 -2
  90. package/dist/client/sharing/ShareButton.js +5 -41
  91. package/dist/client/sharing/ShareDialog.js +5 -4
  92. package/dist/client/use-chat-threads.js +10 -14
  93. package/dist/collab/awareness.d.ts +2 -2
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/collab/struct-routes.d.ts +1 -1
  96. package/dist/localization/default-messages.d.ts +1 -0
  97. package/dist/localization/default-messages.js +1 -0
  98. package/dist/notifications/routes.d.ts +3 -3
  99. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  100. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  101. package/dist/server/auth-marketing.d.ts +1 -0
  102. package/dist/server/auth-marketing.js +0 -2
  103. package/dist/server/auth.d.ts +1 -0
  104. package/dist/server/auth.js +18 -0
  105. package/dist/server/onboarding-html.d.ts +1 -0
  106. package/dist/server/onboarding-html.js +2 -88
  107. package/dist/server/realtime-token.d.ts +1 -1
  108. package/dist/shared/markdown-block-split.d.ts +16 -0
  109. package/dist/shared/markdown-block-split.js +81 -5
  110. package/dist/styles/agent-native.css +13 -0
  111. package/dist/voice/voice-cleanup-prompt.js +1 -0
  112. package/docs/content/cloneable-saas.mdx +24 -26
  113. package/package.json +3 -3
  114. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -19,6 +19,7 @@ import {
19
19
  type H3Event,
20
20
  } from "h3";
21
21
  import { nanoid } from "nanoid";
22
+ import { z } from "zod";
22
23
 
23
24
  import type {
24
25
  Booking,
@@ -33,6 +34,7 @@ import { getDb, schema } from "../db/index.js";
33
34
  import {
34
35
  parseBookingLinkDurations,
35
36
  resolveAvailabilityDuration,
37
+ type BookingDurationSource,
36
38
  } from "../lib/booking-durations.js";
37
39
  import {
38
40
  sendBookingCancellationEmails,
@@ -45,6 +47,7 @@ import {
45
47
  import {
46
48
  getBookingLinkCoHostEmails,
47
49
  getBookingLinkRequiredHostEmails,
50
+ normalizeBookingHosts,
48
51
  } from "../lib/booking-link-utils.js";
49
52
  import { getOwnerBookingTimeZone } from "../lib/booking-timezone.js";
50
53
  import { eventBlocksAvailability } from "../lib/calendar-availability.js";
@@ -173,6 +176,7 @@ type AvailabilityContext = {
173
176
  hostEmails: string[];
174
177
  slug: string;
175
178
  bookingLink?: BookingLinkRow;
179
+ durationSource?: BookingDurationSource;
176
180
  conflictSlugs: string[];
177
181
  };
178
182
 
@@ -182,6 +186,86 @@ type BookingLinkRow = typeof schema.bookingLinks.$inferSelect;
182
186
  type ConflictDb = Pick<ReturnType<typeof getDb>, "select">;
183
187
  const BOOKING_SLOT_STEP_MINUTES = 30;
184
188
 
189
+ const bookingAvailabilityDraftSchema = z
190
+ .object({
191
+ slug: z.string().trim().min(1).max(200),
192
+ durations: z
193
+ .array(
194
+ z
195
+ .number()
196
+ .int()
197
+ .min(1)
198
+ .max(24 * 60),
199
+ )
200
+ .min(1)
201
+ .max(20),
202
+ hosts: z
203
+ .array(
204
+ z.object({
205
+ email: z.string().email().max(320),
206
+ displayName: z.string().max(200).optional(),
207
+ }),
208
+ )
209
+ .max(20),
210
+ })
211
+ .strict();
212
+
213
+ type BookingAvailabilityDraft = z.infer<typeof bookingAvailabilityDraftSchema>;
214
+
215
+ export function parseBookingAvailabilityDraft(
216
+ raw: unknown,
217
+ ): { draft: BookingAvailabilityDraft } | { error: string } {
218
+ if (typeof raw !== "string") {
219
+ return { error: "draft must be a JSON object" };
220
+ }
221
+ if (raw.length > 16_000) {
222
+ return { error: "draft is too large" };
223
+ }
224
+
225
+ let parsed: unknown;
226
+ try {
227
+ parsed = JSON.parse(raw);
228
+ } catch {
229
+ return { error: "draft must be valid JSON" };
230
+ }
231
+
232
+ const result = bookingAvailabilityDraftSchema.safeParse(parsed);
233
+ return result.success
234
+ ? { draft: result.data }
235
+ : { error: "draft has an invalid booking-link configuration" };
236
+ }
237
+
238
+ export function resolveBookingLinkAvailabilityOverrides({
239
+ bookingLink,
240
+ draft,
241
+ }: {
242
+ bookingLink: BookingLinkRow;
243
+ draft?: BookingAvailabilityDraft;
244
+ }): {
245
+ hostEmails: string[];
246
+ durationSource: BookingDurationSource;
247
+ } {
248
+ if (!draft) {
249
+ return {
250
+ hostEmails: getBookingLinkRequiredHostEmails(bookingLink),
251
+ durationSource: bookingLink,
252
+ };
253
+ }
254
+
255
+ const ownerEmail = bookingLink.ownerEmail;
256
+ const draftHosts = normalizeBookingHosts(draft.hosts, ownerEmail);
257
+ return {
258
+ hostEmails: [
259
+ ...(ownerEmail ? [ownerEmail] : []),
260
+ ...draftHosts.map((host) => host.email),
261
+ ],
262
+ durationSource: {
263
+ duration: draft.durations[0],
264
+ durations: JSON.stringify(draft.durations),
265
+ },
266
+ };
267
+ }
268
+
185
269
  type LocalDateTimeParts = {
186
270
  year: number;
187
271
  month: number;
@@ -382,9 +466,11 @@ function formatLocalTime(totalMinutes: number): string {
382
466
 
383
467
  async function resolveAvailabilityContext({
384
468
  slug,
469
+ draft,
385
470
  db = getDb(),
386
471
  }: {
387
472
  slug: string;
473
+ draft?: BookingAvailabilityDraft;
388
474
  db?: ConflictDb;
389
475
  }): Promise<AvailabilityContext> {
390
476
  const [configRaw, bookingLink] = await Promise.all([
@@ -393,17 +479,34 @@ async function resolveAvailabilityContext({
393
479
  ? db
394
480
  .select()
395
481
  .from(schema.bookingLinks)
396
- .where(eq(schema.bookingLinks.slug, slug))
482
+ .where(
483
+ draft
484
+ ? and(
485
+ eq(schema.bookingLinks.slug, slug),
486
+ accessFilter(
487
+ schema.bookingLinks,
488
+ schema.bookingLinkShares,
489
+ undefined,
490
+ "editor",
491
+ ),
492
+ )
493
+ : eq(schema.bookingLinks.slug, slug),
494
+ )
397
495
  .then((rows) => rows[0])
398
496
  : Promise.resolve(undefined),
399
497
  ]);
498
+ if (draft && !bookingLink) {
499
+ throw createError({
500
+ statusCode: 404,
501
+ statusMessage: "Booking link not found",
502
+ });
503
+ }
400
504
  const config = configRaw as unknown as AvailabilityConfig | null;
401
505
  const ownerEmail = bookingLink?.ownerEmail;
402
- const hostEmails = bookingLink
403
- ? getBookingLinkRequiredHostEmails(bookingLink)
404
- : ownerEmail
405
- ? [ownerEmail]
406
- : [];
506
+ const overrides = bookingLink
507
+ ? resolveBookingLinkAvailabilityOverrides({ bookingLink, draft })
508
+ : undefined;
509
+ const hostEmails = overrides?.hostEmails ?? (ownerEmail ? [ownerEmail] : []);
407
510
  const [ownerConfigRaw, ownerSettingsRaw, conflictSlugs] = await Promise.all([
408
511
  ownerEmail
409
512
  ? getUserSetting(ownerEmail, "calendar-availability")
@@ -432,6 +535,7 @@ async function resolveAvailabilityContext({
432
535
  hostEmails,
433
536
  slug,
434
537
  bookingLink,
538
+ durationSource: overrides?.durationSource,
435
539
  conflictSlugs,
436
540
  };
437
541
  }
@@ -1182,111 +1286,138 @@ export const createBooking = defineEventHandler(async (event: H3Event) => {
1182
1286
  }
1183
1287
  });
1184
1288
 
1185
- export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
1186
- try {
1187
- const query = getQuery(event);
1188
- const date = typeof query.date === "string" ? query.date : "";
1189
- const from = parseDateOnly(query.from);
1190
- const to = parseDateOnly(query.to);
1191
- const hasRangeQuery = query.from !== undefined || query.to !== undefined;
1192
- const slug = typeof query.slug === "string" ? query.slug : "";
1193
-
1194
- if (hasRangeQuery) {
1195
- if (!from || !to) {
1196
- setResponseStatus(event, 400);
1197
- return {
1198
- error:
1199
- "from and to query parameters are required together in YYYY-MM-DD format",
1200
- };
1201
- }
1202
- if (from > to) {
1203
- setResponseStatus(event, 400);
1204
- return { error: "from must be before to" };
1205
- }
1206
- if (countDaysInclusive(from, to) > MAX_AVAILABILITY_RANGE_DAYS) {
1207
- setResponseStatus(event, 400);
1208
- return {
1209
- error: `date range cannot exceed ${MAX_AVAILABILITY_RANGE_DAYS} days`,
1210
- };
1211
- }
1212
- } else if (!parseDateOnly(date)) {
1289
+ async function getAvailableSlotsForQuery(
1290
+ event: H3Event,
1291
+ query: Record<string, unknown>,
1292
+ draft?: BookingAvailabilityDraft,
1293
+ ) {
1294
+ const date = typeof query.date === "string" ? query.date : "";
1295
+ const from = parseDateOnly(query.from);
1296
+ const to = parseDateOnly(query.to);
1297
+ const hasRangeQuery = query.from !== undefined || query.to !== undefined;
1298
+ const slug = typeof query.slug === "string" ? query.slug : "";
1299
+
1300
+ if (hasRangeQuery) {
1301
+ if (!from || !to) {
1213
1302
  setResponseStatus(event, 400);
1214
- return { error: "date query parameter is required" };
1215
- }
1216
-
1217
- const context = await resolveAvailabilityContext({ slug });
1218
- if (!context.effectiveConfig) {
1219
- return hasRangeQuery ? { dates: [] } : { slots: [] };
1303
+ return {
1304
+ error:
1305
+ "from and to query parameters are required together in YYYY-MM-DD format",
1306
+ };
1220
1307
  }
1221
- const durationResult = resolveAvailabilityDuration({
1222
- rawDuration: query.duration,
1223
- bookingLink: context.bookingLink,
1224
- availability: context.effectiveConfig,
1225
- });
1226
- if ("error" in durationResult) {
1308
+ if (from > to) {
1227
1309
  setResponseStatus(event, 400);
1228
- return { error: durationResult.error };
1310
+ return { error: "from must be before to" };
1229
1311
  }
1230
- const duration = durationResult.duration;
1231
-
1232
- if (hasRangeQuery) {
1233
- const rangeStart = formatDateOnly(from!);
1234
- const rangeEnd = formatDateOnly(to!);
1235
- const timezone = context.effectiveConfig.timezone || "UTC";
1236
- const conflictResult = await getConflictItems({
1237
- ownerEmail: context.ownerEmail,
1238
- hostEmails: context.hostEmails,
1239
- conflictSlugs: context.conflictSlugs,
1240
- rangeStartIso: dateStartIso(rangeStart, timezone),
1241
- rangeEndIso: dateEndIso(rangeEnd, timezone),
1242
- timezone,
1243
- });
1244
- if (conflictResult.unavailableReason) {
1245
- return unavailableAvailabilityResponse(event);
1246
- }
1247
- const dates: string[] = [];
1248
- for (
1249
- let cursor = new Date(from!);
1250
- cursor <= to!;
1251
- cursor = addLocalDays(cursor, 1)
1252
- ) {
1253
- const day = formatDateOnly(cursor);
1254
- const slots = generateAvailableSlotsForDate({
1255
- date: day,
1256
- duration,
1257
- config: context.effectiveConfig,
1258
- conflictItems: conflictResult.items,
1259
- });
1260
- if (slots.length > 0) {
1261
- dates.push(day);
1262
- }
1263
- }
1264
- return { dates };
1312
+ if (countDaysInclusive(from, to) > MAX_AVAILABILITY_RANGE_DAYS) {
1313
+ setResponseStatus(event, 400);
1314
+ return {
1315
+ error: `date range cannot exceed ${MAX_AVAILABILITY_RANGE_DAYS} days`,
1316
+ };
1265
1317
  }
1318
+ } else if (!parseDateOnly(date)) {
1319
+ setResponseStatus(event, 400);
1320
+ return { error: "date query parameter is required" };
1321
+ }
1322
+
1323
+ const context = await resolveAvailabilityContext({ slug, draft });
1324
+ if (!context.effectiveConfig) {
1325
+ return hasRangeQuery ? { dates: [] } : { slots: [] };
1326
+ }
1327
+ const durationResult = resolveAvailabilityDuration({
1328
+ rawDuration: query.duration,
1329
+ bookingLink: context.durationSource ?? context.bookingLink,
1330
+ availability: context.effectiveConfig,
1331
+ });
1332
+ if ("error" in durationResult) {
1333
+ setResponseStatus(event, 400);
1334
+ return { error: durationResult.error };
1335
+ }
1336
+ const duration = durationResult.duration;
1266
1337
 
1338
+ if (hasRangeQuery) {
1339
+ const rangeStart = formatDateOnly(from!);
1340
+ const rangeEnd = formatDateOnly(to!);
1267
1341
  const timezone = context.effectiveConfig.timezone || "UTC";
1268
1342
  const conflictResult = await getConflictItems({
1269
1343
  ownerEmail: context.ownerEmail,
1270
1344
  hostEmails: context.hostEmails,
1271
1345
  conflictSlugs: context.conflictSlugs,
1272
- rangeStartIso: dateStartIso(date, timezone),
1273
- rangeEndIso: dateEndIso(date, timezone),
1346
+ rangeStartIso: dateStartIso(rangeStart, timezone),
1347
+ rangeEndIso: dateEndIso(rangeEnd, timezone),
1274
1348
  timezone,
1275
1349
  });
1276
1350
  if (conflictResult.unavailableReason) {
1277
1351
  return unavailableAvailabilityResponse(event);
1278
1352
  }
1279
- const availableSlots = generateAvailableSlotsForDate({
1280
- date,
1281
- duration,
1282
- config: context.effectiveConfig,
1283
- conflictItems: conflictResult.items,
1284
- });
1353
+ const dates: string[] = [];
1354
+ for (
1355
+ let cursor = new Date(from!);
1356
+ cursor <= to!;
1357
+ cursor = addLocalDays(cursor, 1)
1358
+ ) {
1359
+ const day = formatDateOnly(cursor);
1360
+ const slots = generateAvailableSlotsForDate({
1361
+ date: day,
1362
+ duration,
1363
+ config: context.effectiveConfig,
1364
+ conflictItems: conflictResult.items,
1365
+ });
1366
+ if (slots.length > 0) {
1367
+ dates.push(day);
1368
+ }
1369
+ }
1370
+ return { dates };
1371
+ }
1285
1372
 
1286
- return { slots: availableSlots };
1373
+ const timezone = context.effectiveConfig.timezone || "UTC";
1374
+ const conflictResult = await getConflictItems({
1375
+ ownerEmail: context.ownerEmail,
1376
+ hostEmails: context.hostEmails,
1377
+ conflictSlugs: context.conflictSlugs,
1378
+ rangeStartIso: dateStartIso(date, timezone),
1379
+ rangeEndIso: dateEndIso(date, timezone),
1380
+ timezone,
1381
+ });
1382
+ if (conflictResult.unavailableReason) {
1383
+ return unavailableAvailabilityResponse(event);
1384
+ }
1385
+ const availableSlots = generateAvailableSlotsForDate({
1386
+ date,
1387
+ duration,
1388
+ config: context.effectiveConfig,
1389
+ conflictItems: conflictResult.items,
1390
+ });
1391
+
1392
+ return { slots: availableSlots };
1393
+ }
1394
+
1395
+ export const getAvailableSlots = defineEventHandler(async (event: H3Event) => {
1396
+ try {
1397
+ const query = getQuery(event);
1398
+ const rawDraft = query.draft;
1399
+ if (rawDraft !== undefined) {
1400
+ const parsedDraft = parseBookingAvailabilityDraft(rawDraft);
1401
+ if ("error" in parsedDraft) {
1402
+ setResponseStatus(event, 400);
1403
+ return { error: parsedDraft.error };
1404
+ }
1405
+ if (typeof query.slug !== "string" || !query.slug) {
1406
+ setResponseStatus(event, 400);
1407
+ return { error: "slug query parameter is required for draft preview" };
1408
+ }
1409
+ return await requireRequestContext(event, () =>
1410
+ getAvailableSlotsForQuery(event, query, parsedDraft.draft),
1411
+ );
1412
+ }
1413
+
1414
+ return await getAvailableSlotsForQuery(event, query);
1287
1415
  } catch (error: any) {
1288
- setResponseStatus(event, 500);
1289
- return { error: error.message };
1416
+ setResponseStatus(
1417
+ event,
1418
+ Number.isInteger(error?.statusCode) ? error.statusCode : 500,
1419
+ );
1420
+ return { error: error?.message || "Failed to fetch available slots" };
1290
1421
  }
1291
1422
  });
1292
1423
 
@@ -16,8 +16,6 @@ export default createAuthPlugin({
16
16
  "Manages availability and booking links automatically",
17
17
  "Answers schedule questions and resolves conflicts instantly",
18
18
  ],
19
- runLocalCommand:
20
- "npx @agent-native/core@latest create my-calendar-app --template calendar",
21
19
  },
22
20
  publicPaths: [
23
21
  "/book",
@@ -1,6 +1,7 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { writeAppState } from "@agent-native/core/application-state";
3
3
  import { and, eq, isNull, sql } from "drizzle-orm";
4
+ import { createError } from "h3";
4
5
  import { z } from "zod";
5
6
 
6
7
  import { getDb, schema } from "../server/db/index.js";
@@ -74,7 +75,12 @@ export default defineAction({
74
75
  .from(schema.folders)
75
76
  .where(and(...parentWhereClauses))
76
77
  .limit(1);
77
- if (!parent) throw new Error(`Parent folder not found: ${args.parentId}`);
78
+ if (!parent) {
79
+ throw createError({
80
+ statusCode: 404,
81
+ statusMessage: `Parent folder not found: ${args.parentId}`,
82
+ });
83
+ }
78
84
  }
79
85
 
80
86
  // Next position within siblings
@@ -78,7 +78,7 @@ export default defineAction({
78
78
  .refine((v) => v.title || v.calendarEventId, {
79
79
  message: "Provide either title or calendarEventId",
80
80
  }),
81
- run: async (args) => {
81
+ run: async (args, actionContext) => {
82
82
  const db = getDb();
83
83
  const ownerEmail = getCurrentOwnerEmail();
84
84
  const orgId = await getActiveOrganizationId();
@@ -191,7 +191,11 @@ export default defineAction({
191
191
  }
192
192
 
193
193
  const visibility =
194
- args.visibility ?? (await getDefaultRecordingVisibility(orgId));
194
+ args.visibility ??
195
+ (await getDefaultRecordingVisibility(
196
+ orgId,
197
+ actionContext?.userEmail ?? ownerEmail,
198
+ ));
195
199
 
196
200
  try {
197
201
  await db.insert(schema.meetings).values({
@@ -39,7 +39,7 @@ export default defineAction({
39
39
  description:
40
40
  "Create a new recording row in 'uploading' status and return its id plus the chunk upload URL template. The frontend POSTs chunks to /api/uploads/:id/chunk?index=N&total=T&isFinal=0|1, then finalizes on the last chunk. Recorders can pass app/window title context for an immediate fallback title.",
41
41
  schema: createRecordingSchema,
42
- run: async (args) => {
42
+ run: async (args, actionContext) => {
43
43
  const db = getDb();
44
44
  const ownerEmail = getCurrentOwnerEmail();
45
45
  const id = args.id || nanoid();
@@ -52,8 +52,10 @@ export default defineAction({
52
52
  const { organizationId } = await requireOrganizationAccess(
53
53
  args.organizationId,
54
54
  );
55
- const defaultVisibility =
56
- await getDefaultRecordingVisibility(organizationId);
55
+ const defaultVisibility = await getDefaultRecordingVisibility(
56
+ organizationId,
57
+ actionContext?.userEmail ?? ownerEmail,
58
+ );
57
59
 
58
60
  const spaceIds = (args.spaceIds ?? []).filter(
59
61
  (value, index, arr) => value && arr.indexOf(value) === index,
@@ -48,6 +48,7 @@ import {
48
48
  deleteResumableSession,
49
49
  getResumableSession,
50
50
  } from "../server/lib/resumable-session.js";
51
+ import { abortResumableUploadSession } from "../server/lib/resumable-upload-cleanup.js";
51
52
  import { resolveResumableUploadProvider } from "../server/lib/resumable-upload-provider.js";
52
53
  import { fetchS3ObjectByUrl } from "../server/lib/s3-upload-provider.js";
53
54
  import {
@@ -1183,17 +1184,35 @@ export default defineAction({
1183
1184
  ),
1184
1185
  );
1185
1186
  }
1186
- try {
1187
- const uploadProvider = await resolveResumableUploadProvider(
1188
- resumableSession.providerId,
1187
+ const uploadProvider = await resolveResumableUploadProvider(
1188
+ resumableSession.providerId,
1189
+ );
1190
+ if (!uploadProvider?.resumable) {
1191
+ throw new Error(
1192
+ "Upload completion failed: No resumable upload provider configured",
1189
1193
  );
1190
- if (!uploadProvider?.resumable) {
1191
- throw new Error("No resumable upload provider configured");
1192
- }
1193
- if (resumableSession.bytesUploaded <= 0) {
1194
- throw new Error("Recording upload contained no video bytes");
1194
+ }
1195
+ if (resumableSession.bytesUploaded <= 0) {
1196
+ const cleaned = await abortResumableUploadSession(resumableSession, {
1197
+ provider: uploadProvider,
1198
+ label: `finalize-${id}`,
1199
+ });
1200
+ if (cleaned) {
1201
+ await deleteResumableSession(id, generationId).catch((deleteErr) =>
1202
+ console.warn(
1203
+ "[finalize] failed to retire empty resumable session:",
1204
+ deleteErr,
1205
+ ),
1206
+ );
1195
1207
  }
1196
- const videoUrl = await uploadProvider.resumable.completeSession(
1208
+ throw new Error(
1209
+ "Upload completion failed: Recording upload contained no video bytes",
1210
+ );
1211
+ }
1212
+
1213
+ let videoUrl: string;
1214
+ try {
1215
+ videoUrl = await uploadProvider.resumable.completeSession(
1197
1216
  {
1198
1217
  sessionId: resumableSession.sessionId,
1199
1218
  meta: resumableSession.meta,
@@ -1203,74 +1222,87 @@ export default defineAction({
1203
1222
  : "",
1204
1223
  { stableUrl: true, recordAsset: false },
1205
1224
  );
1206
- debugLog("[finalize] resumable upload completed", { id, videoUrl });
1207
- let servedBytes: number | null;
1208
- try {
1209
- servedBytes = await verifyServedMediaUrl(id, videoUrl, true);
1210
- } catch (err) {
1211
- const failureReason =
1212
- err instanceof Error ? err.message : String(err);
1213
- const pending = await leaveRecordingProcessingForMediaVerification({
1214
- id,
1215
- ownerEmail,
1216
- failureReason,
1217
- media: {
1218
- videoUrl,
1219
- videoSizeBytes: resumableSession.bytesUploaded,
1220
- sourceSizeBytes: resumableSession.bytesUploaded,
1221
- videoFormat,
1222
- finalDurationMs,
1223
- finalWidth,
1224
- finalHeight,
1225
- finalHasAudio,
1226
- finalHasCamera,
1227
- seekableApplied: false,
1228
- mimeType,
1229
- providerId: resumableSession.providerId,
1230
- locallyTranscoded: args.locallyTranscoded === true,
1231
- },
1232
- });
1225
+ } catch (err) {
1226
+ const cleaned = await abortResumableUploadSession(resumableSession, {
1227
+ provider: uploadProvider,
1228
+ label: `finalize-${id}`,
1229
+ });
1230
+ if (cleaned) {
1233
1231
  await deleteResumableSession(id, generationId).catch((deleteErr) =>
1234
1232
  console.warn(
1235
- "[finalize] failed to retire pending resumable session:",
1233
+ "[finalize] failed to retire aborted resumable session:",
1236
1234
  deleteErr,
1237
1235
  ),
1238
1236
  );
1239
- return pending;
1240
1237
  }
1241
- const result = await markRecordingReady({
1242
- ...readyParams,
1243
- videoUrl,
1244
- videoSizeBytes: servedBytes ?? resumableSession.bytesUploaded,
1245
- sourceSizeBytes: resumableSession.bytesUploaded,
1246
- // Streaming path forwards raw MediaRecorder bytes straight to the
1247
- // provider — no faststart/Cues rewrite happened. Repair in the
1248
- // background.
1249
- seekableApplied: false,
1250
- });
1251
- if (result.status === "ready" && result.transitionedToReady) {
1252
- queueBackgroundBuilderCompression({
1253
- recordingId: id,
1254
- ownerEmail,
1238
+ console.error("[finalize] resumable complete failed:", err);
1239
+ throw new Error(
1240
+ `Upload completion failed: ${err instanceof Error ? err.message : String(err)}`,
1241
+ );
1242
+ }
1243
+
1244
+ debugLog("[finalize] resumable upload completed", { id, videoUrl });
1245
+ let servedBytes: number | null;
1246
+ try {
1247
+ servedBytes = await verifyServedMediaUrl(id, videoUrl, true);
1248
+ } catch (err) {
1249
+ const failureReason =
1250
+ err instanceof Error ? err.message : String(err);
1251
+ const pending = await leaveRecordingProcessingForMediaVerification({
1252
+ id,
1253
+ ownerEmail,
1254
+ failureReason,
1255
+ media: {
1255
1256
  videoUrl,
1257
+ videoSizeBytes: resumableSession.bytesUploaded,
1258
+ sourceSizeBytes: resumableSession.bytesUploaded,
1259
+ videoFormat,
1260
+ finalDurationMs,
1261
+ finalWidth,
1262
+ finalHeight,
1263
+ finalHasAudio,
1264
+ finalHasCamera,
1265
+ seekableApplied: false,
1256
1266
  mimeType,
1257
1267
  providerId: resumableSession.providerId,
1258
- sourceSizeBytes: resumableSession.bytesUploaded,
1259
1268
  locallyTranscoded: args.locallyTranscoded === true,
1260
- });
1261
- }
1262
- // Delete only after durable state is written — so a retry before
1263
- // this point can still find the session and re-enter this path.
1264
- deleteResumableSession(id, generationId).catch((err) =>
1265
- console.warn("[finalize] failed to delete resumable session:", err),
1266
- );
1267
- return result;
1268
- } catch (err) {
1269
- console.error("[finalize] resumable complete failed:", err);
1270
- throw new Error(
1271
- `Upload completion failed: ${err instanceof Error ? err.message : String(err)}`,
1269
+ },
1270
+ });
1271
+ await deleteResumableSession(id, generationId).catch((deleteErr) =>
1272
+ console.warn(
1273
+ "[finalize] failed to retire pending resumable session:",
1274
+ deleteErr,
1275
+ ),
1272
1276
  );
1277
+ return pending;
1273
1278
  }
1279
+ const result = await markRecordingReady({
1280
+ ...readyParams,
1281
+ videoUrl,
1282
+ videoSizeBytes: servedBytes ?? resumableSession.bytesUploaded,
1283
+ sourceSizeBytes: resumableSession.bytesUploaded,
1284
+ // Streaming path forwards raw MediaRecorder bytes straight to the
1285
+ // provider — no faststart/Cues rewrite happened. Repair in the
1286
+ // background.
1287
+ seekableApplied: false,
1288
+ });
1289
+ if (result.status === "ready" && result.transitionedToReady) {
1290
+ queueBackgroundBuilderCompression({
1291
+ recordingId: id,
1292
+ ownerEmail,
1293
+ videoUrl,
1294
+ mimeType,
1295
+ providerId: resumableSession.providerId,
1296
+ sourceSizeBytes: resumableSession.bytesUploaded,
1297
+ locallyTranscoded: args.locallyTranscoded === true,
1298
+ });
1299
+ }
1300
+ // Delete only after durable state is written — so a retry before
1301
+ // this point can still find the session and re-enter this path.
1302
+ deleteResumableSession(id, generationId).catch((err) =>
1303
+ console.warn("[finalize] failed to delete resumable session:", err),
1304
+ );
1305
+ return result;
1274
1306
  }
1275
1307
 
1276
1308
  // Buffered path — assemble chunks from application_state, then upload.