@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
- package/corpus/templates/assets/app/global.css +4 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1 -2
- package/corpus/templates/brain/app/routes/sources.tsx +1 -2
- package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
- package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
- package/corpus/templates/calendar/server/handlers/bookings.ts +224 -93
- package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
- package/corpus/templates/clips/actions/create-folder.ts +7 -1
- package/corpus/templates/clips/actions/create-meeting.ts +6 -2
- package/corpus/templates/clips/actions/create-recording.ts +5 -3
- package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
- package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
- package/corpus/templates/clips/actions/request-recording-access.ts +305 -0
- package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +75 -75
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
- package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +9 -16
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +135 -18
- package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
- package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
- package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
- package/corpus/templates/clips/desktop/src/app.tsx +24 -0
- package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
- package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
- package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
- package/corpus/templates/clips/desktop/src/styles.css +24 -9
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
- package/corpus/templates/clips/server/db/index.ts +1 -0
- package/corpus/templates/clips/server/db/schema.ts +1 -0
- package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
- package/corpus/templates/clips/server/lib/emails.ts +23 -0
- package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
- package/corpus/templates/clips/server/lib/recordings.ts +2 -1
- package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
- package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
- package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
- package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
- package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +133 -4
- package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
- package/corpus/templates/clips/shared/recording-link.ts +4 -0
- package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
- package/corpus/templates/factory/AGENTS.md +5 -1
- package/corpus/templates/factory/README.md +6 -2
- package/corpus/templates/factory/actions/view-screen.ts +15 -0
- package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
- package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
- package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
- package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
- package/corpus/templates/factory/app/routes/agents.tsx +9 -0
- package/corpus/templates/mail/server/plugins/auth.ts +0 -2
- package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
- package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
- package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
- package/corpus/templates/slides/app/context/DeckContext.tsx +71 -22
- package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +13 -0
- package/dist/client/AgentPanel.d.ts +13 -1
- package/dist/client/AgentPanel.js +12 -10
- package/dist/client/AssistantChat.js +25 -5
- package/dist/client/HighlightedCodeBlock.js +12 -2
- package/dist/client/agent-chat/index.d.ts +1 -0
- package/dist/client/agent-chat/index.js +1 -0
- package/dist/client/chat/markdown-renderer.js +16 -3
- package/dist/client/chat-first/apps-rail.js +1 -1
- package/dist/client/components/ui/message-scroller.js +1 -1
- package/dist/client/settings/AccountSettingsCard.js +48 -19
- package/dist/client/sharing/ShareButton.d.ts +1 -2
- package/dist/client/sharing/ShareButton.js +5 -41
- package/dist/client/sharing/ShareDialog.js +5 -4
- package/dist/client/use-chat-threads.js +10 -14
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/localization/default-messages.d.ts +1 -0
- package/dist/localization/default-messages.js +1 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +15 -15
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +0 -2
- package/dist/server/auth.d.ts +1 -0
- package/dist/server/auth.js +18 -0
- package/dist/server/onboarding-html.d.ts +1 -0
- package/dist/server/onboarding-html.js +2 -88
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/shared/markdown-block-split.d.ts +16 -0
- package/dist/shared/markdown-block-split.js +81 -5
- package/dist/styles/agent-native.css +13 -0
- package/dist/voice/voice-cleanup-prompt.js +1 -0
- package/docs/content/cloneable-saas.mdx +24 -26
- package/package.json +3 -3
- 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(
|
|
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
|
|
403
|
-
?
|
|
404
|
-
:
|
|
405
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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 {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
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:
|
|
1310
|
+
return { error: "from must be before to" };
|
|
1229
1311
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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(
|
|
1273
|
-
rangeEndIso: dateEndIso(
|
|
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
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
-
|
|
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(
|
|
1289
|
-
|
|
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)
|
|
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 ??
|
|
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
|
-
|
|
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
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
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
|
|
1233
|
+
"[finalize] failed to retire aborted resumable session:",
|
|
1236
1234
|
deleteErr,
|
|
1237
1235
|
),
|
|
1238
1236
|
);
|
|
1239
|
-
return pending;
|
|
1240
1237
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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.
|