@agent-native/core 0.185.0 → 0.186.0-nightly-20260923174435
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/calendar/actions/delete-event.ts +6 -4
- package/corpus/templates/calendar/actions/delete-events.ts +10 -11
- package/corpus/templates/calendar/actions/event-action-helpers.ts +53 -0
- package/corpus/templates/calendar/actions/get-event.ts +59 -14
- package/corpus/templates/calendar/actions/rsvp-event.ts +5 -3
- package/corpus/templates/calendar/actions/update-event.ts +15 -7
- package/corpus/templates/calendar/actions/update-events.ts +29 -11
- package/corpus/templates/calendar/changelog/2026-09-21-events-stay-editable-when-their-calendar-is-connected-throug.md +6 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +70 -18
- package/corpus/templates/calendar/shared/google-calendar-sources.ts +47 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +35 -1
- package/corpus/templates/clips/app/components/player/delete-recording-menu.tsx +5 -1
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +18 -1
- package/corpus/templates/clips/app/routes/_app.r.$recordingId.tsx +2 -2
- package/corpus/templates/clips/app/routes/record.tsx +1 -0
- package/corpus/templates/clips/changelog/2026-09-23-long-recording-menus-scroll-within-the-visible-space-so-ever.md +6 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +14 -4
- package/corpus/templates/content/app/blocks/contentBlockRegistry.tsx +9 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -2
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +61 -40
- package/corpus/templates/content/app/i18n-data.ts +12 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +36 -9
- package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +1 -1
- package/corpus/templates/content/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/content/shared/labs.ts +35 -1
- package/corpus/templates/slides/actions/get-design-system.ts +4 -2
- package/dist/agent/engine/openai-compatible-endpoint.d.ts +7 -0
- package/dist/agent/engine/openai-compatible-endpoint.js +9 -0
- package/dist/agent/engine/provider-endpoint-validation.d.ts +8 -1
- package/dist/agent/engine/provider-endpoint-validation.js +47 -10
- package/dist/agent/engine/registry.js +8 -5
- package/dist/automation/index.d.ts +1 -1
- package/dist/cli/skills.d.ts +3 -0
- package/dist/cli/skills.js +45 -9
- package/dist/client/agent-engine-key.d.ts +7 -0
- package/dist/client/agent-engine-key.js +37 -0
- package/dist/client/agent-provider-catalog.js +1 -1
- package/dist/client/settings/ProviderSetupForm.js +80 -7
- package/dist/client/settings/SettingsPanel.js +149 -11
- package/dist/client/use-chat-models.js +31 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/labs/actions/set-lab.js +1 -0
- package/dist/labs/registry.js +6 -1
- package/dist/localization/default-messages.d.ts +7 -1
- package/dist/localization/default-messages.js +7 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/index.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +6 -6
- package/dist/provider-api/actions/provider-api.d.ts +3 -3
- package/dist/scripts/agent-engines/test-agent-engine.js +9 -7
- package/dist/secrets/routes.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.js +8 -5
- package/dist/server/agent-engine-ollama-models-route.d.ts +15 -0
- package/dist/server/agent-engine-ollama-models-route.js +105 -0
- package/dist/server/core-routes-plugin.js +5 -0
- package/dist/server/credential-provider.d.ts +13 -0
- package/dist/server/credential-provider.js +19 -0
- package/package.json +3 -3
package/corpus/README.md
CHANGED
|
@@ -9,9 +9,11 @@ import { isGoogleEventAbsentError } from "../server/lib/google-api.js";
|
|
|
9
9
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
10
10
|
import {
|
|
11
11
|
cliBoolean,
|
|
12
|
+
googleEventResultId,
|
|
12
13
|
normalizeWritableGoogleEventId,
|
|
13
14
|
rawCliBoolean,
|
|
14
15
|
requireActionUserEmail,
|
|
16
|
+
resolveGoogleEventAccountEmail,
|
|
15
17
|
resolveOwnedAccountEmail,
|
|
16
18
|
} from "./event-action-helpers.js";
|
|
17
19
|
|
|
@@ -66,11 +68,11 @@ export default defineAction({
|
|
|
66
68
|
);
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
const googleEventId = normalizeWritableGoogleEventId(args.id);
|
|
70
71
|
const accountEmail = await resolveOwnedAccountEmail(
|
|
71
|
-
args.accountEmail,
|
|
72
|
+
resolveGoogleEventAccountEmail(args.id, args.accountEmail),
|
|
72
73
|
ownerEmail,
|
|
73
74
|
);
|
|
75
|
+
const googleEventId = normalizeWritableGoogleEventId(args.id);
|
|
74
76
|
const guestNotificationMessage = normalizeGuestNotificationMessage(
|
|
75
77
|
args.notificationMessage,
|
|
76
78
|
);
|
|
@@ -109,7 +111,7 @@ export default defineAction({
|
|
|
109
111
|
return {
|
|
110
112
|
success: true,
|
|
111
113
|
alreadyAbsent: true,
|
|
112
|
-
id:
|
|
114
|
+
id: googleEventResultId(args.id, googleEventId, accountEmail),
|
|
113
115
|
accountEmail,
|
|
114
116
|
scope: args.scope,
|
|
115
117
|
removedOnly: args.removeOnly ?? false,
|
|
@@ -130,7 +132,7 @@ export default defineAction({
|
|
|
130
132
|
return {
|
|
131
133
|
success: true,
|
|
132
134
|
alreadyAbsent: false,
|
|
133
|
-
id:
|
|
135
|
+
id: googleEventResultId(args.id, googleEventId, accountEmail),
|
|
134
136
|
accountEmail,
|
|
135
137
|
scope: args.scope,
|
|
136
138
|
removedOnly: args.removeOnly ?? false,
|
|
@@ -15,12 +15,14 @@ import {
|
|
|
15
15
|
BULK_EVENT_CONCURRENCY,
|
|
16
16
|
MAX_MATCHED_EVENTS,
|
|
17
17
|
cliBoolean,
|
|
18
|
+
googleEventResultId,
|
|
18
19
|
isBookedOnAccount,
|
|
19
20
|
mapWithConcurrency,
|
|
20
21
|
normalizeWritableGoogleEventId,
|
|
21
22
|
rawCliBoolean,
|
|
22
23
|
requireActionUserEmail,
|
|
23
24
|
requireExplicitBound,
|
|
25
|
+
resolveBulkGoogleEventAccountEmail,
|
|
24
26
|
resolveOwnedAccountEmail,
|
|
25
27
|
startsWithinRange,
|
|
26
28
|
undeletableEventReason,
|
|
@@ -218,22 +220,24 @@ export default defineAction({
|
|
|
218
220
|
|
|
219
221
|
if (hasIds) {
|
|
220
222
|
const accountEmail = await resolveOwnedAccountEmail(
|
|
221
|
-
args.accountEmail,
|
|
223
|
+
resolveBulkGoogleEventAccountEmail(args.ids!, args.accountEmail),
|
|
222
224
|
ownerEmail,
|
|
223
225
|
);
|
|
224
226
|
// Two spellings of one id ("google-a" and "a") would otherwise enqueue two
|
|
225
227
|
// writes for the same event: one succeeds, the other 404s, and the report
|
|
226
228
|
// claims a failure that never happened.
|
|
227
229
|
const requested = Array.from(
|
|
228
|
-
new
|
|
230
|
+
new Map(
|
|
231
|
+
args.ids!.map((id) => [normalizeWritableGoogleEventId(id), id]),
|
|
232
|
+
).entries(),
|
|
229
233
|
);
|
|
230
234
|
// Explicit ids get the same booking protection as a filtered selection:
|
|
231
235
|
// naming the event directly does not make leaving its booking confirmed
|
|
232
236
|
// any less of a silent inconsistency.
|
|
233
|
-
const booked = await findBookedGoogleEvents(requested);
|
|
234
|
-
for (const googleEventId of requested) {
|
|
237
|
+
const booked = await findBookedGoogleEvents(requested.map(([id]) => id));
|
|
238
|
+
for (const [googleEventId, displayId] of requested) {
|
|
235
239
|
const display: BulkEventResult = {
|
|
236
|
-
id:
|
|
240
|
+
id: googleEventResultId(displayId, googleEventId, accountEmail),
|
|
237
241
|
accountEmail,
|
|
238
242
|
outcome: "matched",
|
|
239
243
|
};
|
|
@@ -290,12 +294,7 @@ export default defineAction({
|
|
|
290
294
|
|
|
291
295
|
for (const event of matched) {
|
|
292
296
|
const display: BulkEventResult = {
|
|
293
|
-
id:
|
|
294
|
-
event.overlayEmail || event.calendarReadOnly
|
|
295
|
-
? event.id
|
|
296
|
-
: event.googleEventId
|
|
297
|
-
? `google-${event.googleEventId}`
|
|
298
|
-
: event.id,
|
|
297
|
+
id: event.id,
|
|
299
298
|
title: event.title,
|
|
300
299
|
start: event.start,
|
|
301
300
|
weekday: eventWeekday(event.start, range.timezone),
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
} from "../server/lib/find-time.js";
|
|
13
13
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
14
14
|
import type { CalendarEvent } from "../shared/api.js";
|
|
15
|
+
import {
|
|
16
|
+
createGoogleAccountEventId,
|
|
17
|
+
parseGoogleAccountEventId,
|
|
18
|
+
} from "../shared/google-calendar-sources.js";
|
|
15
19
|
|
|
16
20
|
export const cliBoolean = z
|
|
17
21
|
.union([z.boolean(), z.enum(["true", "false"])])
|
|
@@ -190,9 +194,58 @@ export function requireActionUserEmail(): string {
|
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
export function normalizeGoogleEventId(id: string): string {
|
|
197
|
+
const accountEvent = parseGoogleAccountEventId(id);
|
|
198
|
+
if (accountEvent) return accountEvent.googleEventId;
|
|
193
199
|
return id.startsWith("google-") ? id.slice("google-".length) : id;
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
export function googleEventResultId(
|
|
203
|
+
inputId: string,
|
|
204
|
+
googleEventId: string,
|
|
205
|
+
accountEmail: string,
|
|
206
|
+
): string {
|
|
207
|
+
return parseGoogleAccountEventId(inputId)
|
|
208
|
+
? createGoogleAccountEventId({ accountEmail, googleEventId })
|
|
209
|
+
: `google-${googleEventId}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function resolveGoogleEventAccountEmail(
|
|
213
|
+
id: string,
|
|
214
|
+
accountEmail: string | undefined,
|
|
215
|
+
): string | undefined {
|
|
216
|
+
const accountEvent = parseGoogleAccountEventId(id);
|
|
217
|
+
if (!accountEvent) return accountEmail;
|
|
218
|
+
if (
|
|
219
|
+
accountEmail &&
|
|
220
|
+
accountEmail.trim().toLowerCase() !== accountEvent.accountEmail
|
|
221
|
+
) {
|
|
222
|
+
throw new Error("Google event account does not match the selected account");
|
|
223
|
+
}
|
|
224
|
+
return accountEvent.accountEmail;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function resolveBulkGoogleEventAccountEmail(
|
|
228
|
+
ids: string[],
|
|
229
|
+
accountEmail: string | undefined,
|
|
230
|
+
): string | undefined {
|
|
231
|
+
const scopedCount = ids.filter((id) => parseGoogleAccountEventId(id)).length;
|
|
232
|
+
if (scopedCount > 0 && scopedCount !== ids.length) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
"Bulk event ids cannot mix account-scoped and legacy Google ids",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const accounts = new Set(
|
|
238
|
+
ids
|
|
239
|
+
.map((id) => resolveGoogleEventAccountEmail(id, accountEmail))
|
|
240
|
+
.filter((email): email is string => !!email)
|
|
241
|
+
.map((email) => email.trim().toLowerCase()),
|
|
242
|
+
);
|
|
243
|
+
if (accounts.size > 1) {
|
|
244
|
+
throw new Error("Bulk event ids must belong to one Google account");
|
|
245
|
+
}
|
|
246
|
+
return accounts.values().next().value ?? accountEmail;
|
|
247
|
+
}
|
|
248
|
+
|
|
196
249
|
export function normalizeWritableGoogleEventId(id: string): string {
|
|
197
250
|
if (id.startsWith("overlay-") && id.slice("overlay-".length).includes("@")) {
|
|
198
251
|
throw new Error("Overlay Google calendar events are read-only");
|
|
@@ -5,7 +5,11 @@ import { z } from "zod";
|
|
|
5
5
|
import { calendarGetEvent } from "../server/lib/google-api.js";
|
|
6
6
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
7
7
|
import type { CalendarEvent } from "../shared/api.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
createGoogleAccountEventId,
|
|
10
|
+
parseGoogleAccountEventId,
|
|
11
|
+
parseGoogleCalendarSourceKey,
|
|
12
|
+
} from "../shared/google-calendar-sources.js";
|
|
9
13
|
import { getGoogleEventColorHex } from "../shared/google-event-colors.js";
|
|
10
14
|
|
|
11
15
|
export default defineAction({
|
|
@@ -61,26 +65,37 @@ export default defineAction({
|
|
|
61
65
|
run: async (args) => {
|
|
62
66
|
const email = getRequestUserEmail();
|
|
63
67
|
if (!email) throw new Error("no authenticated user");
|
|
68
|
+
const accountEvent = parseGoogleAccountEventId(args.id);
|
|
64
69
|
|
|
65
70
|
if (args.calendarSourceKey) {
|
|
66
71
|
const source = parseGoogleCalendarSourceKey(args.calendarSourceKey);
|
|
67
72
|
if (!source) throw new Error("Invalid Google Calendar source key");
|
|
73
|
+
if (accountEvent && accountEvent.accountEmail !== source.accountEmail) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"Google event account does not match the selected source",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
68
78
|
const namespacedPrefix = `google-${args.calendarSourceKey}-`;
|
|
69
|
-
const rawId =
|
|
70
|
-
?
|
|
71
|
-
: args.id.startsWith(
|
|
72
|
-
? args.id.slice(
|
|
73
|
-
: args.id
|
|
74
|
-
|
|
79
|
+
const rawId = accountEvent
|
|
80
|
+
? accountEvent.googleEventId
|
|
81
|
+
: args.id.startsWith(namespacedPrefix)
|
|
82
|
+
? args.id.slice(namespacedPrefix.length)
|
|
83
|
+
: args.id.startsWith("google-")
|
|
84
|
+
? args.id.slice("google-".length)
|
|
85
|
+
: args.id;
|
|
86
|
+
const event = await googleCalendar.getEvent(
|
|
75
87
|
rawId,
|
|
76
88
|
{ ownerEmail: email, accountEmail: source.accountEmail },
|
|
77
89
|
{ calendarSourceKey: args.calendarSourceKey },
|
|
78
90
|
);
|
|
91
|
+
return accountEvent ? { ...event, id: args.id } : event;
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
const rawId =
|
|
82
|
-
?
|
|
83
|
-
: args.id
|
|
94
|
+
const rawId = accountEvent
|
|
95
|
+
? accountEvent.googleEventId
|
|
96
|
+
: args.id.startsWith("google-")
|
|
97
|
+
? args.id.slice("google-".length)
|
|
98
|
+
: args.id;
|
|
84
99
|
const calendarId = args.calendarId ?? "primary";
|
|
85
100
|
if (calendarId !== "primary") {
|
|
86
101
|
throw new Error(
|
|
@@ -88,20 +103,49 @@ export default defineAction({
|
|
|
88
103
|
);
|
|
89
104
|
}
|
|
90
105
|
|
|
91
|
-
const clients =
|
|
106
|
+
const { clients, errors } = accountEvent
|
|
107
|
+
? await googleCalendar.getClientsWithErrors(email)
|
|
108
|
+
: { clients: await googleCalendar.getClients(email), errors: [] };
|
|
109
|
+
const accountError = accountEvent
|
|
110
|
+
? errors.find(
|
|
111
|
+
({ email: accountEmail }) =>
|
|
112
|
+
accountEmail.trim().toLowerCase() === accountEvent.accountEmail,
|
|
113
|
+
)
|
|
114
|
+
: undefined;
|
|
115
|
+
if (accountError) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Google Calendar connection for ${accountEvent!.accountEmail} failed: ${accountError.error}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
92
120
|
if (clients.length === 0) {
|
|
93
121
|
return {
|
|
94
122
|
error: "Google Calendar not connected. Connect via Settings first.",
|
|
95
123
|
};
|
|
96
124
|
}
|
|
97
125
|
|
|
98
|
-
|
|
126
|
+
const selectedClients = accountEvent
|
|
127
|
+
? clients.filter(
|
|
128
|
+
({ email: accountEmail }) =>
|
|
129
|
+
accountEmail.trim().toLowerCase() === accountEvent.accountEmail,
|
|
130
|
+
)
|
|
131
|
+
: clients;
|
|
132
|
+
if (accountEvent && selectedClients.length === 0) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Google Calendar account is not connected: ${accountEvent.accountEmail}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
for (const { email: acctEmail, accessToken } of selectedClients) {
|
|
99
138
|
try {
|
|
100
139
|
const evt = await calendarGetEvent(accessToken, calendarId, rawId);
|
|
101
140
|
const selfAttendee = evt.attendees?.find((a: any) => a.self === true);
|
|
102
141
|
|
|
103
142
|
const calEvent: CalendarEvent = {
|
|
104
|
-
id:
|
|
143
|
+
id: accountEvent
|
|
144
|
+
? createGoogleAccountEventId({
|
|
145
|
+
accountEmail: acctEmail,
|
|
146
|
+
googleEventId: evt.id,
|
|
147
|
+
})
|
|
148
|
+
: `google-${evt.id}`,
|
|
105
149
|
title: evt.summary || "Untitled",
|
|
106
150
|
titleIsGenerated: !evt.summary,
|
|
107
151
|
description: evt.description || "",
|
|
@@ -185,7 +229,8 @@ export default defineAction({
|
|
|
185
229
|
};
|
|
186
230
|
|
|
187
231
|
return calEvent;
|
|
188
|
-
} catch {
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (accountEvent) throw error;
|
|
189
234
|
// Try next account
|
|
190
235
|
continue;
|
|
191
236
|
}
|
|
@@ -3,8 +3,10 @@ import { z } from "zod";
|
|
|
3
3
|
|
|
4
4
|
import * as googleCalendar from "../server/lib/google-calendar.js";
|
|
5
5
|
import {
|
|
6
|
+
googleEventResultId,
|
|
6
7
|
normalizeWritableGoogleEventId,
|
|
7
8
|
requireActionUserEmail,
|
|
9
|
+
resolveGoogleEventAccountEmail,
|
|
8
10
|
resolveOwnedAccountEmail,
|
|
9
11
|
} from "./event-action-helpers.js";
|
|
10
12
|
|
|
@@ -49,11 +51,11 @@ export default defineAction({
|
|
|
49
51
|
);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
const googleEventId = normalizeWritableGoogleEventId(args.id);
|
|
53
54
|
const accountEmail = await resolveOwnedAccountEmail(
|
|
54
|
-
args.accountEmail,
|
|
55
|
+
resolveGoogleEventAccountEmail(args.id, args.accountEmail),
|
|
55
56
|
ownerEmail,
|
|
56
57
|
);
|
|
58
|
+
const googleEventId = normalizeWritableGoogleEventId(args.id);
|
|
57
59
|
|
|
58
60
|
await googleCalendar.rsvpEvent(
|
|
59
61
|
googleEventId,
|
|
@@ -66,7 +68,7 @@ export default defineAction({
|
|
|
66
68
|
|
|
67
69
|
return {
|
|
68
70
|
success: true,
|
|
69
|
-
id:
|
|
71
|
+
id: googleEventResultId(args.id, googleEventId, accountEmail),
|
|
70
72
|
accountEmail,
|
|
71
73
|
status: args.status,
|
|
72
74
|
note: args.note?.trim() ?? args.note,
|
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
cliBoolean,
|
|
21
21
|
googleColorIdInput,
|
|
22
22
|
normalizeAttendees,
|
|
23
|
+
googleEventResultId,
|
|
23
24
|
normalizeWritableGoogleEventId,
|
|
24
25
|
normalizeRecurrence,
|
|
25
26
|
reminderMethodInput,
|
|
26
27
|
reminderMinutesInput,
|
|
27
28
|
remindersInput,
|
|
28
29
|
requireActionUserEmail,
|
|
30
|
+
resolveGoogleEventAccountEmail,
|
|
29
31
|
resolveOwnedAccountEmail,
|
|
30
32
|
validateEventTimeOrder,
|
|
31
33
|
validateStatusEventTiming,
|
|
@@ -256,11 +258,11 @@ export default defineAction({
|
|
|
256
258
|
);
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
const googleEventId = normalizeWritableGoogleEventId(args.id);
|
|
260
261
|
const accountEmail = await resolveOwnedAccountEmail(
|
|
261
|
-
args.accountEmail,
|
|
262
|
+
resolveGoogleEventAccountEmail(args.id, args.accountEmail),
|
|
262
263
|
ownerEmail,
|
|
263
264
|
);
|
|
265
|
+
const googleEventId = normalizeWritableGoogleEventId(args.id);
|
|
264
266
|
const targetAccountEmail =
|
|
265
267
|
args.targetAccountEmail !== undefined
|
|
266
268
|
? await resolveOwnedAccountEmail(args.targetAccountEmail, ownerEmail)
|
|
@@ -445,8 +447,8 @@ export default defineAction({
|
|
|
445
447
|
);
|
|
446
448
|
return {
|
|
447
449
|
success: true,
|
|
448
|
-
id:
|
|
449
|
-
replacedId:
|
|
450
|
+
id: googleEventResultId(args.id, result.id, targetAccountEmail!),
|
|
451
|
+
replacedId: googleEventResultId(args.id, googleEventId, accountEmail),
|
|
450
452
|
accountEmail: targetAccountEmail,
|
|
451
453
|
updated: ["accountEmail"],
|
|
452
454
|
htmlLink: result.htmlLink,
|
|
@@ -585,7 +587,7 @@ export default defineAction({
|
|
|
585
587
|
if (updatedKeys.length === 0 && zoomAlreadyPresent) {
|
|
586
588
|
return {
|
|
587
589
|
success: true,
|
|
588
|
-
id:
|
|
590
|
+
id: googleEventResultId(args.id, googleEventId, accountEmail),
|
|
589
591
|
accountEmail,
|
|
590
592
|
updated: [],
|
|
591
593
|
meetingLink: zoomMeetingLink,
|
|
@@ -746,9 +748,15 @@ export default defineAction({
|
|
|
746
748
|
|
|
747
749
|
return {
|
|
748
750
|
success: true,
|
|
749
|
-
id:
|
|
751
|
+
id: googleEventResultId(args.id, returnedGoogleEventId, accountEmail),
|
|
750
752
|
...(returnedGoogleEventId !== googleEventId
|
|
751
|
-
? {
|
|
753
|
+
? {
|
|
754
|
+
replacedId: googleEventResultId(
|
|
755
|
+
args.id,
|
|
756
|
+
googleEventId,
|
|
757
|
+
accountEmail,
|
|
758
|
+
),
|
|
759
|
+
}
|
|
752
760
|
: {}),
|
|
753
761
|
accountEmail,
|
|
754
762
|
updated: updatedKeys,
|
|
@@ -13,11 +13,13 @@ import {
|
|
|
13
13
|
BULK_EVENT_CONCURRENCY,
|
|
14
14
|
MAX_MATCHED_EVENTS,
|
|
15
15
|
cliBoolean,
|
|
16
|
+
googleEventResultId,
|
|
16
17
|
isBookedOnAccount,
|
|
17
18
|
mapWithConcurrency,
|
|
18
19
|
normalizeWritableGoogleEventId,
|
|
19
20
|
requireActionUserEmail,
|
|
20
21
|
requireExplicitBound,
|
|
22
|
+
resolveBulkGoogleEventAccountEmail,
|
|
21
23
|
validateEventTimeOrder,
|
|
22
24
|
resolveOwnedAccountEmail,
|
|
23
25
|
startsWithinRange,
|
|
@@ -188,22 +190,24 @@ export default defineAction({
|
|
|
188
190
|
|
|
189
191
|
if (hasIds) {
|
|
190
192
|
const accountEmail = await resolveOwnedAccountEmail(
|
|
191
|
-
args.accountEmail,
|
|
193
|
+
resolveBulkGoogleEventAccountEmail(args.ids!, args.accountEmail),
|
|
192
194
|
ownerEmail,
|
|
193
195
|
);
|
|
194
196
|
const requested = Array.from(
|
|
195
|
-
new
|
|
197
|
+
new Map(
|
|
198
|
+
args.ids!.map((id) => [normalizeWritableGoogleEventId(id), id]),
|
|
199
|
+
).entries(),
|
|
196
200
|
);
|
|
197
|
-
for (const id of requested) {
|
|
201
|
+
for (const [id, displayId] of requested) {
|
|
198
202
|
try {
|
|
199
203
|
const event = await googleCalendar.getEvent(id, {
|
|
200
204
|
ownerEmail,
|
|
201
205
|
accountEmail,
|
|
202
206
|
});
|
|
203
|
-
events.push({ event, accountEmail });
|
|
207
|
+
events.push({ event: { ...event, id: displayId }, accountEmail });
|
|
204
208
|
} catch (error) {
|
|
205
209
|
skipped.push({
|
|
206
|
-
id:
|
|
210
|
+
id: googleEventResultId(displayId, id, accountEmail),
|
|
207
211
|
accountEmail,
|
|
208
212
|
outcome: "failed",
|
|
209
213
|
reason: isGoogleNotFoundError(error)
|
|
@@ -243,7 +247,13 @@ export default defineAction({
|
|
|
243
247
|
);
|
|
244
248
|
for (const event of matched) {
|
|
245
249
|
const result: BulkEventResult = {
|
|
246
|
-
id: event.googleEventId
|
|
250
|
+
id: event.googleEventId
|
|
251
|
+
? googleEventResultId(
|
|
252
|
+
event.id,
|
|
253
|
+
event.googleEventId,
|
|
254
|
+
event.accountEmail ?? ownerEmail,
|
|
255
|
+
)
|
|
256
|
+
: event.id,
|
|
247
257
|
title: event.title,
|
|
248
258
|
start: event.start,
|
|
249
259
|
end: event.end,
|
|
@@ -291,7 +301,7 @@ export default defineAction({
|
|
|
291
301
|
}
|
|
292
302
|
if (isBookedOnAccount(booked, event.googleEventId, accountEmail)) {
|
|
293
303
|
skipped.push({
|
|
294
|
-
id:
|
|
304
|
+
id: googleEventResultId(event.id, event.googleEventId, accountEmail),
|
|
295
305
|
title: event.title,
|
|
296
306
|
start: event.start,
|
|
297
307
|
end: event.end,
|
|
@@ -303,7 +313,7 @@ export default defineAction({
|
|
|
303
313
|
}
|
|
304
314
|
if (args.shiftMinutes !== undefined && event.allDay) {
|
|
305
315
|
skipped.push({
|
|
306
|
-
id:
|
|
316
|
+
id: googleEventResultId(event.id, event.googleEventId, accountEmail),
|
|
307
317
|
title: event.title,
|
|
308
318
|
start: event.start,
|
|
309
319
|
end: event.end,
|
|
@@ -321,7 +331,7 @@ export default defineAction({
|
|
|
321
331
|
const range = effectiveRange(event, args);
|
|
322
332
|
assertEffectiveRangeOrdered(event, range);
|
|
323
333
|
return {
|
|
324
|
-
id:
|
|
334
|
+
id: googleEventResultId(event.id, event.googleEventId!, accountEmail),
|
|
325
335
|
title: event.title,
|
|
326
336
|
start: range.start,
|
|
327
337
|
end: range.end,
|
|
@@ -360,7 +370,11 @@ export default defineAction({
|
|
|
360
370
|
},
|
|
361
371
|
);
|
|
362
372
|
return {
|
|
363
|
-
id:
|
|
373
|
+
id: googleEventResultId(
|
|
374
|
+
event.id,
|
|
375
|
+
event.googleEventId!,
|
|
376
|
+
accountEmail,
|
|
377
|
+
),
|
|
364
378
|
title: event.title,
|
|
365
379
|
start,
|
|
366
380
|
end,
|
|
@@ -369,7 +383,11 @@ export default defineAction({
|
|
|
369
383
|
};
|
|
370
384
|
} catch (error) {
|
|
371
385
|
return {
|
|
372
|
-
id:
|
|
386
|
+
id: googleEventResultId(
|
|
387
|
+
event.id,
|
|
388
|
+
event.googleEventId!,
|
|
389
|
+
accountEmail,
|
|
390
|
+
),
|
|
373
391
|
title: event.title,
|
|
374
392
|
start,
|
|
375
393
|
end,
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
UpdateEventScope,
|
|
23
23
|
} from "../../shared/api.js";
|
|
24
24
|
import {
|
|
25
|
+
createGoogleAccountEventId,
|
|
25
26
|
createGoogleCalendarCanonicalKey,
|
|
26
27
|
createGoogleCalendarSourceKey,
|
|
27
28
|
} from "../../shared/google-calendar-sources.js";
|
|
@@ -1087,6 +1088,26 @@ function compareCalendarSourcePaths(
|
|
|
1087
1088
|
return a.accountEmail.localeCompare(b.accountEmail);
|
|
1088
1089
|
}
|
|
1089
1090
|
|
|
1091
|
+
function compareCalendarEventSources(
|
|
1092
|
+
a: CalendarEvent,
|
|
1093
|
+
b: CalendarEvent,
|
|
1094
|
+
): number {
|
|
1095
|
+
const writable =
|
|
1096
|
+
Number(b.calendarReadOnly === false) - Number(a.calendarReadOnly === false);
|
|
1097
|
+
if (writable !== 0) return writable;
|
|
1098
|
+
|
|
1099
|
+
const primary =
|
|
1100
|
+
Number(b.calendarPrimary === true) - Number(a.calendarPrimary === true);
|
|
1101
|
+
if (primary !== 0) return primary;
|
|
1102
|
+
|
|
1103
|
+
const access =
|
|
1104
|
+
(CALENDAR_ACCESS_RANK[b.calendarAccessRole ?? "freeBusyReader"] ?? -1) -
|
|
1105
|
+
(CALENDAR_ACCESS_RANK[a.calendarAccessRole ?? "freeBusyReader"] ?? -1);
|
|
1106
|
+
if (access !== 0) return access;
|
|
1107
|
+
|
|
1108
|
+
return (a.accountEmail ?? "").localeCompare(b.accountEmail ?? "");
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1090
1111
|
/**
|
|
1091
1112
|
* Resolve a client-supplied canonical source identity against the user's live
|
|
1092
1113
|
* CalendarList. Provider paths stay server-selected, so a stale or forged
|
|
@@ -1097,9 +1118,17 @@ export async function resolveGoogleCalendarSource(
|
|
|
1097
1118
|
sourceKey: string,
|
|
1098
1119
|
): Promise<GoogleCalendarSource> {
|
|
1099
1120
|
const discovered = await listGoogleCalendars(ownerEmail);
|
|
1100
|
-
const source = discovered.calendars
|
|
1101
|
-
(candidate) =>
|
|
1102
|
-
|
|
1121
|
+
const source = discovered.calendars
|
|
1122
|
+
.flatMap((candidate) =>
|
|
1123
|
+
(candidate.sourcePaths ?? [candidate]).map((path) => ({
|
|
1124
|
+
...candidate,
|
|
1125
|
+
...path,
|
|
1126
|
+
readOnly:
|
|
1127
|
+
path.primary !== true ||
|
|
1128
|
+
(path.accessRole !== "owner" && path.accessRole !== "writer"),
|
|
1129
|
+
})),
|
|
1130
|
+
)
|
|
1131
|
+
.find((candidate) => candidate.sourceKey === sourceKey);
|
|
1103
1132
|
if (!source) {
|
|
1104
1133
|
throw new Error(
|
|
1105
1134
|
"Google Calendar source is not connected or no longer available",
|
|
@@ -1331,6 +1360,8 @@ export async function listEvents(
|
|
|
1331
1360
|
// "calendar is empty" and the user sees no error.
|
|
1332
1361
|
const errors: Array<{ email: string; error: string }> = [...refreshErrors];
|
|
1333
1362
|
if (clients.length === 0) return { events: [], errors };
|
|
1363
|
+
const hasMultipleOwnedAccounts =
|
|
1364
|
+
clients.length > 1 || (await getOwnedAccountEmails(forEmail)).length > 1;
|
|
1334
1365
|
|
|
1335
1366
|
const requestedSourceKeys = Array.from(
|
|
1336
1367
|
new Set((options.calendarSourceKeys ?? []).filter(Boolean)),
|
|
@@ -1339,9 +1370,27 @@ export async function listEvents(
|
|
|
1339
1370
|
if (requestedSourceKeys.length > 0) {
|
|
1340
1371
|
const discovered = await listGoogleCalendars(forEmail);
|
|
1341
1372
|
errors.push(...discovered.errors);
|
|
1342
|
-
const discoveredByKey = new Map
|
|
1343
|
-
|
|
1344
|
-
|
|
1373
|
+
const discoveredByKey = new Map<
|
|
1374
|
+
string,
|
|
1375
|
+
{
|
|
1376
|
+
source: GoogleCalendarSource;
|
|
1377
|
+
paths: Array<
|
|
1378
|
+
Pick<
|
|
1379
|
+
GoogleCalendarSource,
|
|
1380
|
+
"sourceKey" | "accountEmail" | "accessRole" | "primary"
|
|
1381
|
+
>
|
|
1382
|
+
>;
|
|
1383
|
+
}
|
|
1384
|
+
>();
|
|
1385
|
+
for (const source of discovered.calendars) {
|
|
1386
|
+
const paths = source.sourcePaths ?? [source];
|
|
1387
|
+
discoveredByKey.set(source.sourceKey, { source, paths });
|
|
1388
|
+
for (const path of paths) {
|
|
1389
|
+
if (path.sourceKey !== source.sourceKey) {
|
|
1390
|
+
discoveredByKey.set(path.sourceKey, { source, paths: [path] });
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1345
1394
|
const invalid = requestedSourceKeys.filter(
|
|
1346
1395
|
(sourceKey) => !discoveredByKey.has(sourceKey),
|
|
1347
1396
|
);
|
|
@@ -1351,21 +1400,14 @@ export async function listEvents(
|
|
|
1351
1400
|
);
|
|
1352
1401
|
}
|
|
1353
1402
|
for (const sourceKey of requestedSourceKeys) {
|
|
1354
|
-
const source = discoveredByKey.get(sourceKey)!;
|
|
1355
|
-
const paths = source.sourcePaths ?? [
|
|
1356
|
-
{
|
|
1357
|
-
sourceKey: source.sourceKey,
|
|
1358
|
-
accountEmail: source.accountEmail,
|
|
1359
|
-
accessRole: source.accessRole,
|
|
1360
|
-
primary: source.primary,
|
|
1361
|
-
},
|
|
1362
|
-
];
|
|
1403
|
+
const { source, paths } = discoveredByKey.get(sourceKey)!;
|
|
1363
1404
|
for (const path of paths) {
|
|
1364
1405
|
const accountKey = path.accountEmail.trim().toLowerCase();
|
|
1365
1406
|
selectedSourcesByAccount.set(accountKey, [
|
|
1366
1407
|
...(selectedSourcesByAccount.get(accountKey) ?? []),
|
|
1367
1408
|
{
|
|
1368
1409
|
...source,
|
|
1410
|
+
sourceKey: path.sourceKey,
|
|
1369
1411
|
accountEmail: path.accountEmail,
|
|
1370
1412
|
accessRole: path.accessRole,
|
|
1371
1413
|
primary: path.primary,
|
|
@@ -1457,7 +1499,12 @@ export async function listEvents(
|
|
|
1457
1499
|
id:
|
|
1458
1500
|
calendarSource && !calendarSource.primary
|
|
1459
1501
|
? `google-${calendarSource.sourceKey}-${event.id}`
|
|
1460
|
-
:
|
|
1502
|
+
: hasMultipleOwnedAccounts
|
|
1503
|
+
? createGoogleAccountEventId({
|
|
1504
|
+
accountEmail: email,
|
|
1505
|
+
googleEventId: event.id,
|
|
1506
|
+
})
|
|
1507
|
+
: `google-${event.id}`,
|
|
1461
1508
|
title: event.summary || "Untitled",
|
|
1462
1509
|
titleIsGenerated: !event.summary,
|
|
1463
1510
|
description: event.description || "",
|
|
@@ -1538,8 +1585,13 @@ export async function listEvents(
|
|
|
1538
1585
|
const key =
|
|
1539
1586
|
event.canonicalKey && event.googleEventId
|
|
1540
1587
|
? `${event.canonicalKey}:${event.googleEventId}`
|
|
1541
|
-
: event.
|
|
1542
|
-
|
|
1588
|
+
: event.googleEventId && event.accountEmail
|
|
1589
|
+
? `google-account:${event.accountEmail.toLowerCase()}:${event.googleEventId}`
|
|
1590
|
+
: event.id;
|
|
1591
|
+
const existing = dedupedEvents.get(key);
|
|
1592
|
+
if (!existing || compareCalendarEventSources(event, existing) < 0) {
|
|
1593
|
+
dedupedEvents.set(key, event);
|
|
1594
|
+
}
|
|
1543
1595
|
}
|
|
1544
1596
|
return { events: Array.from(dedupedEvents.values()), errors };
|
|
1545
1597
|
}
|